mirror of https://github.com/encounter/objdiff.git
Initial commit
This commit is contained in:
commit
cb3c6062c7
|
@ -0,0 +1,61 @@
|
||||||
|
name: Build
|
||||||
|
|
||||||
|
on: [ push, pull_request ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
name: Check
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
toolchain: [ stable, 1.61.0, nightly ]
|
||||||
|
fail-fast: false
|
||||||
|
env:
|
||||||
|
RUSTFLAGS: -D warnings
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
profile: minimal
|
||||||
|
toolchain: ${{ matrix.toolchain }}
|
||||||
|
override: true
|
||||||
|
components: rustfmt, clippy
|
||||||
|
- uses: EmbarkStudios/cargo-deny-action@v1
|
||||||
|
- uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
command: check
|
||||||
|
args: --all-features
|
||||||
|
- uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
command: clippy
|
||||||
|
args: --all-features
|
||||||
|
|
||||||
|
build:
|
||||||
|
name: Build
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
platform: [ ubuntu-latest, macos-latest, windows-latest ]
|
||||||
|
toolchain: [ stable, 1.61.0, nightly ]
|
||||||
|
fail-fast: false
|
||||||
|
runs-on: ${{ matrix.platform }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
profile: minimal
|
||||||
|
toolchain: ${{ matrix.toolchain }}
|
||||||
|
override: true
|
||||||
|
- uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
command: test
|
||||||
|
args: --release --all-features
|
||||||
|
- uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
command: build
|
||||||
|
args: --release --all-features
|
||||||
|
- uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: ${{ matrix.platform }}-${{ matrix.toolchain }}
|
||||||
|
path: |
|
||||||
|
target/release/objdiff
|
||||||
|
target/release/objdiff.exe
|
|
@ -0,0 +1,24 @@
|
||||||
|
# Rust
|
||||||
|
target/
|
||||||
|
**/*.rs.bk
|
||||||
|
generated/
|
||||||
|
|
||||||
|
# cargo-mobile
|
||||||
|
.cargo/
|
||||||
|
/gen
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# JetBrains
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Generated SPIR-V
|
||||||
|
*.spv
|
||||||
|
|
||||||
|
# project
|
||||||
|
textures/
|
||||||
|
android.keystore
|
||||||
|
*.frag
|
||||||
|
*.vert
|
||||||
|
*.metal
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,36 @@
|
||||||
|
[package]
|
||||||
|
name = "objdiff"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
rust-version = "1.61"
|
||||||
|
authors = ["Luke Street <luke@street.dev>"]
|
||||||
|
license = "MIT OR Apache-2.0"
|
||||||
|
repository = "https://github.com/encounter/objdiff"
|
||||||
|
readme = "README.md"
|
||||||
|
description = """
|
||||||
|
A tool for decompilation projects.
|
||||||
|
"""
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
egui = "0.19.0"
|
||||||
|
eframe = { version = "0.19.0", features = ["persistence"] } # , "wgpu"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
anyhow = "1.0.63"
|
||||||
|
thiserror = "1.0.33"
|
||||||
|
flagset = "0.4.3"
|
||||||
|
object = "0.29.0"
|
||||||
|
notify = "5.0.0"
|
||||||
|
cwdemangle = "0.1.1"
|
||||||
|
log = "0.4.17"
|
||||||
|
rfd = { version = "0.10.0" } # , default-features = false, features = ['xdg-portal']
|
||||||
|
egui_extras = "0.19.0"
|
||||||
|
ppc750cl = { git = "https://github.com/terorie/ppc750cl" }
|
||||||
|
|
||||||
|
# native:
|
||||||
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
|
tracing-subscriber = "0.3"
|
||||||
|
|
||||||
|
# web:
|
||||||
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
|
console_error_panic_hook = "0.1.6"
|
||||||
|
tracing-wasm = "0.2"
|
|
@ -0,0 +1,28 @@
|
||||||
|
# objdiff [![Build Status]][actions]
|
||||||
|
|
||||||
|
[Build Status]: https://github.com/encounter/objdiff/actions/workflows/build.yaml/badge.svg
|
||||||
|
[actions]: https://github.com/encounter/objdiff/actions
|
||||||
|
|
||||||
|
A tool for decompilation projects.
|
||||||
|
|
||||||
|
Currently supports:
|
||||||
|
- PowerPC 750CL (GameCube & Wii)
|
||||||
|
|
||||||
|
**WARNING:** Very early & unstable.
|
||||||
|
|
||||||
|
![Screenshot](assets/screen-diff.png)
|
||||||
|
|
||||||
|
### License
|
||||||
|
|
||||||
|
Licensed under either of
|
||||||
|
|
||||||
|
* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
|
||||||
|
* MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
|
||||||
|
|
||||||
|
at your option.
|
||||||
|
|
||||||
|
### Contribution
|
||||||
|
|
||||||
|
Unless you explicitly state otherwise, any contribution intentionally submitted
|
||||||
|
for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any
|
||||||
|
additional terms or conditions.
|
Binary file not shown.
After Width: | Height: | Size: 133 KiB |
|
@ -0,0 +1,210 @@
|
||||||
|
# This template contains all of the possible sections and their default values
|
||||||
|
|
||||||
|
# Note that all fields that take a lint level have these possible values:
|
||||||
|
# * deny - An error will be produced and the check will fail
|
||||||
|
# * warn - A warning will be produced, but the check will not fail
|
||||||
|
# * allow - No warning or error will be produced, though in some cases a note
|
||||||
|
# will be
|
||||||
|
|
||||||
|
# The values provided in this template are the default values that will be used
|
||||||
|
# when any section or field is not specified in your own configuration
|
||||||
|
|
||||||
|
# If 1 or more target triples (and optionally, target_features) are specified,
|
||||||
|
# only the specified targets will be checked when running `cargo deny check`.
|
||||||
|
# This means, if a particular package is only ever used as a target specific
|
||||||
|
# dependency, such as, for example, the `nix` crate only being used via the
|
||||||
|
# `target_family = "unix"` configuration, that only having windows targets in
|
||||||
|
# this list would mean the nix crate, as well as any of its exclusive
|
||||||
|
# dependencies not shared by any other crates, would be ignored, as the target
|
||||||
|
# list here is effectively saying which targets you are building for.
|
||||||
|
targets = [
|
||||||
|
# The triple can be any string, but only the target triples built in to
|
||||||
|
# rustc (as of 1.40) can be checked against actual config expressions
|
||||||
|
#{ triple = "x86_64-unknown-linux-musl" },
|
||||||
|
# You can also specify which target_features you promise are enabled for a
|
||||||
|
# particular target. target_features are currently not validated against
|
||||||
|
# the actual valid features supported by the target architecture.
|
||||||
|
#{ triple = "wasm32-unknown-unknown", features = ["atomics"] },
|
||||||
|
]
|
||||||
|
|
||||||
|
# This section is considered when running `cargo deny check advisories`
|
||||||
|
# More documentation for the advisories section can be found here:
|
||||||
|
# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html
|
||||||
|
[advisories]
|
||||||
|
# The path where the advisory database is cloned/fetched into
|
||||||
|
db-path = "~/.cargo/advisory-db"
|
||||||
|
# The url(s) of the advisory databases to use
|
||||||
|
db-urls = ["https://github.com/rustsec/advisory-db"]
|
||||||
|
# The lint level for security vulnerabilities
|
||||||
|
vulnerability = "deny"
|
||||||
|
# The lint level for unmaintained crates
|
||||||
|
unmaintained = "warn"
|
||||||
|
# The lint level for crates that have been yanked from their source registry
|
||||||
|
yanked = "warn"
|
||||||
|
# The lint level for crates with security notices. Note that as of
|
||||||
|
# 2019-12-17 there are no security notice advisories in
|
||||||
|
# https://github.com/rustsec/advisory-db
|
||||||
|
notice = "warn"
|
||||||
|
# A list of advisory IDs to ignore. Note that ignored advisories will still
|
||||||
|
# output a note when they are encountered.
|
||||||
|
ignore = [
|
||||||
|
#"RUSTSEC-0000-0000",
|
||||||
|
]
|
||||||
|
# Threshold for security vulnerabilities, any vulnerability with a CVSS score
|
||||||
|
# lower than the range specified will be ignored. Note that ignored advisories
|
||||||
|
# will still output a note when they are encountered.
|
||||||
|
# * None - CVSS Score 0.0
|
||||||
|
# * Low - CVSS Score 0.1 - 3.9
|
||||||
|
# * Medium - CVSS Score 4.0 - 6.9
|
||||||
|
# * High - CVSS Score 7.0 - 8.9
|
||||||
|
# * Critical - CVSS Score 9.0 - 10.0
|
||||||
|
#severity-threshold =
|
||||||
|
|
||||||
|
# This section is considered when running `cargo deny check licenses`
|
||||||
|
# More documentation for the licenses section can be found here:
|
||||||
|
# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html
|
||||||
|
[licenses]
|
||||||
|
# The lint level for crates which do not have a detectable license
|
||||||
|
unlicensed = "deny"
|
||||||
|
# List of explictly allowed licenses
|
||||||
|
# See https://spdx.org/licenses/ for list of possible licenses
|
||||||
|
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
|
||||||
|
allow = [
|
||||||
|
"MIT",
|
||||||
|
"Apache-2.0",
|
||||||
|
"ISC",
|
||||||
|
"BSD-2-Clause",
|
||||||
|
"BSD-3-Clause",
|
||||||
|
"BSL-1.0",
|
||||||
|
"CC0-1.0",
|
||||||
|
"MPL-2.0",
|
||||||
|
"Unicode-DFS-2016",
|
||||||
|
"Zlib",
|
||||||
|
]
|
||||||
|
# List of explictly disallowed licenses
|
||||||
|
# See https://spdx.org/licenses/ for list of possible licenses
|
||||||
|
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
|
||||||
|
deny = [
|
||||||
|
#"Nokia",
|
||||||
|
]
|
||||||
|
# Lint level for licenses considered copyleft
|
||||||
|
copyleft = "warn"
|
||||||
|
# Blanket approval or denial for OSI-approved or FSF Free/Libre licenses
|
||||||
|
# * both - The license will be approved if it is both OSI-approved *AND* FSF
|
||||||
|
# * either - The license will be approved if it is either OSI-approved *OR* FSF
|
||||||
|
# * osi-only - The license will be approved if is OSI-approved *AND NOT* FSF
|
||||||
|
# * fsf-only - The license will be approved if is FSF *AND NOT* OSI-approved
|
||||||
|
# * neither - This predicate is ignored and the default lint level is used
|
||||||
|
allow-osi-fsf-free = "neither"
|
||||||
|
# Lint level used when no other predicates are matched
|
||||||
|
# 1. License isn't in the allow or deny lists
|
||||||
|
# 2. License isn't copyleft
|
||||||
|
# 3. License isn't OSI/FSF, or allow-osi-fsf-free = "neither"
|
||||||
|
default = "deny"
|
||||||
|
# The confidence threshold for detecting a license from license text.
|
||||||
|
# The higher the value, the more closely the license text must be to the
|
||||||
|
# canonical license text of a valid SPDX license file.
|
||||||
|
# [possible values: any between 0.0 and 1.0].
|
||||||
|
confidence-threshold = 0.8
|
||||||
|
# Allow 1 or more licenses on a per-crate basis, so that particular licenses
|
||||||
|
# aren't accepted for every possible crate as with the normal allow list
|
||||||
|
exceptions = [
|
||||||
|
# Each entry is the crate and version constraint, and its specific allow
|
||||||
|
# list
|
||||||
|
#{ allow = ["Zlib"], name = "adler32", version = "*" },
|
||||||
|
]
|
||||||
|
|
||||||
|
# Some crates don't have (easily) machine readable licensing information,
|
||||||
|
# adding a clarification entry for it allows you to manually specify the
|
||||||
|
# licensing information
|
||||||
|
#[[licenses.clarify]]
|
||||||
|
# The name of the crate the clarification applies to
|
||||||
|
#name = "ring"
|
||||||
|
# The optional version constraint for the crate
|
||||||
|
#version = "*"
|
||||||
|
# The SPDX expression for the license requirements of the crate
|
||||||
|
#expression = "MIT AND ISC AND OpenSSL"
|
||||||
|
# One or more files in the crate's source used as the "source of truth" for
|
||||||
|
# the license expression. If the contents match, the clarification will be used
|
||||||
|
# when running the license check, otherwise the clarification will be ignored
|
||||||
|
# and the crate will be checked normally, which may produce warnings or errors
|
||||||
|
# depending on the rest of your configuration
|
||||||
|
#license-files = [
|
||||||
|
# Each entry is a crate relative path, and the (opaque) hash of its contents
|
||||||
|
#{ path = "LICENSE", hash = 0xbd0eed23 }
|
||||||
|
#]
|
||||||
|
|
||||||
|
[licenses.private]
|
||||||
|
# If true, ignores workspace crates that aren't published, or are only
|
||||||
|
# published to private registries
|
||||||
|
ignore = false
|
||||||
|
# One or more private registries that you might publish crates to, if a crate
|
||||||
|
# is only published to private registries, and ignore is true, the crate will
|
||||||
|
# not have its license(s) checked
|
||||||
|
registries = [
|
||||||
|
#"https://sekretz.com/registry
|
||||||
|
]
|
||||||
|
|
||||||
|
# This section is considered when running `cargo deny check bans`.
|
||||||
|
# More documentation about the 'bans' section can be found here:
|
||||||
|
# https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html
|
||||||
|
[bans]
|
||||||
|
# Lint level for when multiple versions of the same crate are detected
|
||||||
|
multiple-versions = "warn"
|
||||||
|
# Lint level for when a crate version requirement is `*`
|
||||||
|
wildcards = "allow"
|
||||||
|
# The graph highlighting used when creating dotgraphs for crates
|
||||||
|
# with multiple versions
|
||||||
|
# * lowest-version - The path to the lowest versioned duplicate is highlighted
|
||||||
|
# * simplest-path - The path to the version with the fewest edges is highlighted
|
||||||
|
# * all - Both lowest-version and simplest-path are used
|
||||||
|
highlight = "all"
|
||||||
|
# List of crates that are allowed. Use with care!
|
||||||
|
allow = [
|
||||||
|
#{ name = "ansi_term", version = "=0.11.0" },
|
||||||
|
]
|
||||||
|
# List of crates to deny
|
||||||
|
deny = [
|
||||||
|
# Each entry the name of a crate and a version range. If version is
|
||||||
|
# not specified, all versions will be matched.
|
||||||
|
#{ name = "ansi_term", version = "=0.11.0" },
|
||||||
|
#
|
||||||
|
# Wrapper crates can optionally be specified to allow the crate when it
|
||||||
|
# is a direct dependency of the otherwise banned crate
|
||||||
|
#{ name = "ansi_term", version = "=0.11.0", wrappers = [] },
|
||||||
|
]
|
||||||
|
# Certain crates/versions that will be skipped when doing duplicate detection.
|
||||||
|
skip = [
|
||||||
|
#{ name = "ansi_term", version = "=0.11.0" },
|
||||||
|
]
|
||||||
|
# Similarly to `skip` allows you to skip certain crates during duplicate
|
||||||
|
# detection. Unlike skip, it also includes the entire tree of transitive
|
||||||
|
# dependencies starting at the specified crate, up to a certain depth, which is
|
||||||
|
# by default infinite
|
||||||
|
skip-tree = [
|
||||||
|
#{ name = "ansi_term", version = "=0.11.0", depth = 20 },
|
||||||
|
]
|
||||||
|
|
||||||
|
# This section is considered when running `cargo deny check sources`.
|
||||||
|
# More documentation about the 'sources' section can be found here:
|
||||||
|
# https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html
|
||||||
|
[sources]
|
||||||
|
# Lint level for what to happen when a crate from a crate registry that is not
|
||||||
|
# in the allow list is encountered
|
||||||
|
unknown-registry = "warn"
|
||||||
|
# Lint level for what to happen when a crate from a git repository that is not
|
||||||
|
# in the allow list is encountered
|
||||||
|
unknown-git = "warn"
|
||||||
|
# List of URLs for allowed crate registries. Defaults to the crates.io index
|
||||||
|
# if not specified. If it is specified but empty, no registries are allowed.
|
||||||
|
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
|
||||||
|
# List of URLs for allowed Git repositories
|
||||||
|
allow-git = []
|
||||||
|
|
||||||
|
[sources.allow-org]
|
||||||
|
# 1 or more github.com organizations to allow git sources for
|
||||||
|
github = ["encounter", "terorie"]
|
||||||
|
# 1 or more gitlab.com organizations to allow git sources for
|
||||||
|
#gitlab = [""]
|
||||||
|
# 1 or more bitbucket.org organizations to allow git sources for
|
||||||
|
#bitbucket = [""]
|
|
@ -0,0 +1,8 @@
|
||||||
|
fn_single_line = true
|
||||||
|
group_imports = "StdExternalCrate"
|
||||||
|
imports_granularity = "Crate"
|
||||||
|
overflow_delimited_expr = true
|
||||||
|
reorder_impl_items = true
|
||||||
|
use_field_init_shorthand = true
|
||||||
|
use_small_heuristics = "Max"
|
||||||
|
where_single_line = true
|
|
@ -0,0 +1,266 @@
|
||||||
|
use std::{
|
||||||
|
default::Default,
|
||||||
|
ffi::OsStr,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
sync::{
|
||||||
|
atomic::{AtomicBool, Ordering},
|
||||||
|
Arc, RwLock,
|
||||||
|
},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
|
use eframe::Frame;
|
||||||
|
use notify::{RecursiveMode, Watcher};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
jobs::{
|
||||||
|
build::{queue_build, BuildResult},
|
||||||
|
Job, JobResult, JobState,
|
||||||
|
},
|
||||||
|
views::{
|
||||||
|
config::config_ui, function_diff::function_diff_ui, jobs::jobs_ui,
|
||||||
|
symbol_diff::symbol_diff_ui,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Default, Eq, PartialEq)]
|
||||||
|
pub enum View {
|
||||||
|
#[default]
|
||||||
|
SymbolDiff,
|
||||||
|
FunctionDiff,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, serde::Deserialize, serde::Serialize)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct ViewState {
|
||||||
|
#[serde(skip)]
|
||||||
|
pub jobs: Vec<JobState>,
|
||||||
|
#[serde(skip)]
|
||||||
|
pub build: Option<Box<BuildResult>>,
|
||||||
|
#[serde(skip)]
|
||||||
|
pub highlighted_symbol: Option<String>,
|
||||||
|
#[serde(skip)]
|
||||||
|
pub selected_symbol: Option<String>,
|
||||||
|
#[serde(skip)]
|
||||||
|
pub current_view: View,
|
||||||
|
// Config
|
||||||
|
pub reverse_fn_order: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Clone, serde::Deserialize, serde::Serialize)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct AppConfig {
|
||||||
|
pub project_dir: Option<PathBuf>,
|
||||||
|
pub build_asm_dir: Option<PathBuf>,
|
||||||
|
pub build_src_dir: Option<PathBuf>,
|
||||||
|
pub build_obj: Option<String>,
|
||||||
|
#[serde(skip)]
|
||||||
|
pub project_dir_change: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// We derive Deserialize/Serialize so we can persist app state on shutdown.
|
||||||
|
#[derive(serde::Deserialize, serde::Serialize)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct App {
|
||||||
|
view_state: ViewState,
|
||||||
|
#[serde(skip)]
|
||||||
|
config: Arc<RwLock<AppConfig>>,
|
||||||
|
#[serde(skip)]
|
||||||
|
modified: Arc<AtomicBool>,
|
||||||
|
#[serde(skip)]
|
||||||
|
watcher: Option<notify::RecommendedWatcher>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for App {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
view_state: ViewState::default(),
|
||||||
|
config: Arc::new(Default::default()),
|
||||||
|
modified: Arc::new(Default::default()),
|
||||||
|
watcher: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const CONFIG_KEY: &str = "app_config";
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
/// Called once before the first frame.
|
||||||
|
pub fn new(cc: &eframe::CreationContext<'_>) -> Self {
|
||||||
|
// This is also where you can customized the look at feel of egui using
|
||||||
|
// `cc.egui_ctx.set_visuals` and `cc.egui_ctx.set_fonts`.
|
||||||
|
|
||||||
|
// Load previous app state (if any).
|
||||||
|
// Note that you must enable the `persistence` feature for this to work.
|
||||||
|
if let Some(storage) = cc.storage {
|
||||||
|
let mut app: App = eframe::get_value(storage, eframe::APP_KEY).unwrap_or_default();
|
||||||
|
let mut config: AppConfig = eframe::get_value(storage, CONFIG_KEY).unwrap_or_default();
|
||||||
|
if config.project_dir.is_some() {
|
||||||
|
config.project_dir_change = true;
|
||||||
|
}
|
||||||
|
app.config = Arc::new(RwLock::new(config));
|
||||||
|
app
|
||||||
|
} else {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl eframe::App for App {
|
||||||
|
/// Called each time the UI needs repainting, which may be many times per second.
|
||||||
|
/// Put your widgets into a `SidePanel`, `TopPanel`, `CentralPanel`, `Window` or `Area`.
|
||||||
|
fn update(&mut self, ctx: &egui::Context, frame: &mut Frame) {
|
||||||
|
let Self { config, view_state, .. } = self;
|
||||||
|
|
||||||
|
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
|
||||||
|
egui::menu::bar(ui, |ui| {
|
||||||
|
ui.menu_button("File", |ui| {
|
||||||
|
if ui.button("Quit").clicked() {
|
||||||
|
frame.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if view_state.current_view == View::FunctionDiff
|
||||||
|
&& matches!(&view_state.build, Some(b) if b.first_status.success && b.second_status.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| {
|
||||||
|
function_diff_ui(ui, view_state);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
egui::SidePanel::left("side_panel").show(ctx, |ui| {
|
||||||
|
ui.heading("Config");
|
||||||
|
config_ui(ui, config, view_state);
|
||||||
|
jobs_ui(ui, view_state);
|
||||||
|
});
|
||||||
|
|
||||||
|
egui::CentralPanel::default().show(ctx, |ui| {
|
||||||
|
symbol_diff_ui(ui, view_state);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if view_state.jobs.iter().any(|job| {
|
||||||
|
if let Some(handle) = &job.handle {
|
||||||
|
return !handle.is_finished();
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}) {
|
||||||
|
ctx.request_repaint();
|
||||||
|
} else {
|
||||||
|
ctx.request_repaint();
|
||||||
|
ctx.request_repaint_after(Duration::from_millis(100));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called by the frame work to save state before shutdown.
|
||||||
|
fn save(&mut self, storage: &mut dyn eframe::Storage) {
|
||||||
|
if let Ok(config) = self.config.read() {
|
||||||
|
eframe::set_value(storage, CONFIG_KEY, &*config);
|
||||||
|
}
|
||||||
|
eframe::set_value(storage, eframe::APP_KEY, self);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn post_rendering(&mut self, _window_size_px: [u32; 2], _frame: &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::Build(state) => {
|
||||||
|
self.view_state.build = Some(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to join job handle: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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() {
|
||||||
|
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 let Some(build_obj) = &config.build_obj {
|
||||||
|
if self.modified.load(Ordering::Relaxed) {
|
||||||
|
if !self
|
||||||
|
.view_state
|
||||||
|
.jobs
|
||||||
|
.iter()
|
||||||
|
.any(|j| j.job_type == Job::Build && j.handle.is_some())
|
||||||
|
{
|
||||||
|
self.view_state
|
||||||
|
.jobs
|
||||||
|
.push(queue_build(build_obj.clone(), self.config.clone()));
|
||||||
|
}
|
||||||
|
self.modified.store(false, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_watcher(
|
||||||
|
modified: Arc<AtomicBool>,
|
||||||
|
project_dir: &Path,
|
||||||
|
) -> notify::Result<notify::RecommendedWatcher> {
|
||||||
|
let mut watcher =
|
||||||
|
notify::recommended_watcher(move |res: notify::Result<notify::Event>| match res {
|
||||||
|
Ok(event) => {
|
||||||
|
if matches!(event.kind, notify::EventKind::Modify(..)) {
|
||||||
|
let watch_extensions = &[
|
||||||
|
Some(OsStr::new("c")),
|
||||||
|
Some(OsStr::new("cp")),
|
||||||
|
Some(OsStr::new("cpp")),
|
||||||
|
Some(OsStr::new("h")),
|
||||||
|
Some(OsStr::new("hpp")),
|
||||||
|
];
|
||||||
|
if event.paths.iter().any(|p| watch_extensions.contains(&p.extension())) {
|
||||||
|
modified.store(true, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => println!("watch error: {:?}", e),
|
||||||
|
})?;
|
||||||
|
watcher.watch(project_dir, RecursiveMode::Recursive)?;
|
||||||
|
Ok(watcher)
|
||||||
|
}
|
|
@ -0,0 +1,396 @@
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use ppc750cl::{disasm_iter, Argument};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
editops::{editops_find, LevEditType},
|
||||||
|
obj::{
|
||||||
|
ObjInfo, ObjIns, ObjInsArg, ObjInsArgDiff, ObjInsBranchFrom, ObjInsBranchTo, ObjInsDiff,
|
||||||
|
ObjInsDiffKind, ObjReloc, ObjRelocKind, ObjSection, ObjSectionKind, ObjSymbol,
|
||||||
|
ObjSymbolFlags,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Relative relocation, can be Simm or BranchDest
|
||||||
|
fn is_relative_arg(arg: &ObjInsArg) -> bool {
|
||||||
|
matches!(arg, ObjInsArg::Arg(arg) if matches!(arg, Argument::Simm(_) | Argument::BranchDest(_)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Relative or absolute relocation, can be Uimm, Simm or Offset
|
||||||
|
fn is_rel_abs_arg(arg: &ObjInsArg) -> bool {
|
||||||
|
matches!(arg, ObjInsArg::Arg(arg) if matches!(arg, Argument::Uimm(_) | Argument::Simm(_) | Argument::Offset(_)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_offset_arg(arg: &ObjInsArg) -> bool { matches!(arg, ObjInsArg::Arg(Argument::Offset(_))) }
|
||||||
|
|
||||||
|
fn process_code(data: &[u8], address: u64, relocs: &[ObjReloc]) -> Result<(Vec<u8>, Vec<ObjIns>)> {
|
||||||
|
let ins_count = data.len() / 4;
|
||||||
|
let mut ops = Vec::<u8>::with_capacity(ins_count);
|
||||||
|
let mut insts = Vec::<ObjIns>::with_capacity(ins_count);
|
||||||
|
for mut ins in disasm_iter(data, address as u32) {
|
||||||
|
let reloc = relocs.iter().find(|r| (r.address as u32 & !3) == ins.addr);
|
||||||
|
if let Some(reloc) = reloc {
|
||||||
|
// Zero out relocations
|
||||||
|
ins.code = match reloc.kind {
|
||||||
|
ObjRelocKind::PpcEmbSda21 => ins.code & !0x1FFFFF,
|
||||||
|
ObjRelocKind::PpcRel24 => ins.code & !0x3FFFFFC,
|
||||||
|
ObjRelocKind::PpcRel14 => ins.code & !0xFFFC,
|
||||||
|
ObjRelocKind::PpcAddr16Hi
|
||||||
|
| ObjRelocKind::PpcAddr16Ha
|
||||||
|
| ObjRelocKind::PpcAddr16Lo => ins.code & !0xFFFF,
|
||||||
|
_ => ins.code,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let simplified = ins.simplified();
|
||||||
|
let mut args: Vec<ObjInsArg> =
|
||||||
|
simplified.args.iter().map(|a| ObjInsArg::Arg(a.clone())).collect();
|
||||||
|
if let Some(reloc) = reloc {
|
||||||
|
match reloc.kind {
|
||||||
|
ObjRelocKind::PpcEmbSda21 => {
|
||||||
|
args = vec![args[0].clone(), ObjInsArg::Reloc];
|
||||||
|
}
|
||||||
|
ObjRelocKind::PpcRel24 | ObjRelocKind::PpcRel14 => {
|
||||||
|
let arg = args
|
||||||
|
.iter_mut()
|
||||||
|
.rfind(|a| is_relative_arg(a))
|
||||||
|
.ok_or_else(|| anyhow::Error::msg("Failed to locate rel arg for reloc"))?;
|
||||||
|
*arg = ObjInsArg::Reloc;
|
||||||
|
}
|
||||||
|
ObjRelocKind::PpcAddr16Hi
|
||||||
|
| ObjRelocKind::PpcAddr16Ha
|
||||||
|
| ObjRelocKind::PpcAddr16Lo => {
|
||||||
|
let arg = args.iter_mut().rfind(|a| is_rel_abs_arg(a)).ok_or_else(|| {
|
||||||
|
anyhow::Error::msg("Failed to locate rel/abs arg for reloc")
|
||||||
|
})?;
|
||||||
|
*arg =
|
||||||
|
if is_offset_arg(arg) { ObjInsArg::RelocOffset } else { ObjInsArg::Reloc };
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ops.push(simplified.ins.op as u8);
|
||||||
|
let suffix = simplified.ins.suffix();
|
||||||
|
insts.push(ObjIns {
|
||||||
|
ins: simplified.ins,
|
||||||
|
mnemonic: format!("{}{}", simplified.mnemonic, suffix),
|
||||||
|
args,
|
||||||
|
reloc: reloc.cloned(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok((ops, insts))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn diff_code(
|
||||||
|
left_data: &[u8],
|
||||||
|
right_data: &[u8],
|
||||||
|
left_symbol: &mut ObjSymbol,
|
||||||
|
right_symbol: &mut ObjSymbol,
|
||||||
|
left_relocs: &[ObjReloc],
|
||||||
|
right_relocs: &[ObjReloc],
|
||||||
|
) -> Result<()> {
|
||||||
|
let left_code =
|
||||||
|
&left_data[left_symbol.address as usize..(left_symbol.address + left_symbol.size) as usize];
|
||||||
|
let (left_ops, left_insts) = process_code(left_code, left_symbol.address, left_relocs)?;
|
||||||
|
let right_code = &right_data
|
||||||
|
[right_symbol.address as usize..(right_symbol.address + right_symbol.size) as usize];
|
||||||
|
let (right_ops, right_insts) = process_code(right_code, right_symbol.address, right_relocs)?;
|
||||||
|
|
||||||
|
let mut left_diff = Vec::<ObjInsDiff>::new();
|
||||||
|
let mut right_diff = Vec::<ObjInsDiff>::new();
|
||||||
|
let edit_ops = editops_find(&left_ops, &right_ops);
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut op_iter = edit_ops.iter();
|
||||||
|
let mut left_iter = left_insts.iter();
|
||||||
|
let mut right_iter = right_insts.iter();
|
||||||
|
let mut cur_op = op_iter.next();
|
||||||
|
let mut cur_left = left_iter.next();
|
||||||
|
let mut cur_right = right_iter.next();
|
||||||
|
while let Some(op) = cur_op {
|
||||||
|
let left_addr = op.first_start as u32 * 4;
|
||||||
|
let right_addr = op.second_start as u32 * 4;
|
||||||
|
while let (Some(left), Some(right)) = (cur_left, cur_right) {
|
||||||
|
if (left.ins.addr - left_symbol.address as u32) < left_addr {
|
||||||
|
left_diff.push(ObjInsDiff { ins: Some(left.clone()), ..ObjInsDiff::default() });
|
||||||
|
right_diff
|
||||||
|
.push(ObjInsDiff { ins: Some(right.clone()), ..ObjInsDiff::default() });
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
cur_left = left_iter.next();
|
||||||
|
cur_right = right_iter.next();
|
||||||
|
}
|
||||||
|
if let (Some(left), Some(right)) = (cur_left, cur_right) {
|
||||||
|
if (left.ins.addr - left_symbol.address as u32) != left_addr {
|
||||||
|
return Err(anyhow::Error::msg("Instruction address mismatch (left)"));
|
||||||
|
}
|
||||||
|
if (right.ins.addr - right_symbol.address as u32) != right_addr {
|
||||||
|
return Err(anyhow::Error::msg("Instruction address mismatch (right)"));
|
||||||
|
}
|
||||||
|
match op.op_type {
|
||||||
|
LevEditType::Replace => {
|
||||||
|
left_diff
|
||||||
|
.push(ObjInsDiff { ins: Some(left.clone()), ..ObjInsDiff::default() });
|
||||||
|
right_diff
|
||||||
|
.push(ObjInsDiff { ins: Some(right.clone()), ..ObjInsDiff::default() });
|
||||||
|
cur_left = left_iter.next();
|
||||||
|
cur_right = right_iter.next();
|
||||||
|
}
|
||||||
|
LevEditType::Insert => {
|
||||||
|
left_diff.push(ObjInsDiff::default());
|
||||||
|
right_diff
|
||||||
|
.push(ObjInsDiff { ins: Some(right.clone()), ..ObjInsDiff::default() });
|
||||||
|
cur_right = right_iter.next();
|
||||||
|
}
|
||||||
|
LevEditType::Delete => {
|
||||||
|
left_diff
|
||||||
|
.push(ObjInsDiff { ins: Some(left.clone()), ..ObjInsDiff::default() });
|
||||||
|
right_diff.push(ObjInsDiff::default());
|
||||||
|
cur_left = left_iter.next();
|
||||||
|
}
|
||||||
|
LevEditType::Keep => unreachable!(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
cur_op = op_iter.next();
|
||||||
|
}
|
||||||
|
// Finalize
|
||||||
|
while cur_left.is_some() || cur_right.is_some() {
|
||||||
|
left_diff.push(ObjInsDiff { ins: cur_left.cloned(), ..ObjInsDiff::default() });
|
||||||
|
right_diff.push(ObjInsDiff { ins: cur_right.cloned(), ..ObjInsDiff::default() });
|
||||||
|
cur_left = left_iter.next();
|
||||||
|
cur_right = right_iter.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve_branches(&mut left_diff);
|
||||||
|
resolve_branches(&mut right_diff);
|
||||||
|
|
||||||
|
let mut diff_state = InsDiffState::default();
|
||||||
|
for (left, right) in left_diff.iter_mut().zip(right_diff.iter_mut()) {
|
||||||
|
let result = compare_ins(left, right, &mut diff_state)?;
|
||||||
|
left.kind = result.kind;
|
||||||
|
right.kind = result.kind;
|
||||||
|
left.arg_diff = result.left_args_diff;
|
||||||
|
right.arg_diff = result.right_args_diff;
|
||||||
|
}
|
||||||
|
|
||||||
|
let total = left_insts.len();
|
||||||
|
let percent = ((total - diff_state.diff_count) as f32 / total as f32) * 100.0;
|
||||||
|
left_symbol.match_percent = percent;
|
||||||
|
right_symbol.match_percent = percent;
|
||||||
|
|
||||||
|
left_symbol.instructions = left_diff;
|
||||||
|
right_symbol.instructions = right_diff;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_branches(vec: &mut [ObjInsDiff]) {
|
||||||
|
let mut branch_idx = 0usize;
|
||||||
|
// Map addresses to indices
|
||||||
|
let mut addr_map = BTreeMap::<u32, usize>::new();
|
||||||
|
for (i, ins_diff) in vec.iter().enumerate() {
|
||||||
|
if let Some(ins) = &ins_diff.ins {
|
||||||
|
addr_map.insert(ins.ins.addr, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Generate branches
|
||||||
|
let mut branches = BTreeMap::<usize, ObjInsBranchFrom>::new();
|
||||||
|
for (i, ins_diff) in vec.iter_mut().enumerate() {
|
||||||
|
if let Some(ins) = &ins_diff.ins {
|
||||||
|
if ins.ins.is_blr() || ins.reloc.is_some() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(ins_idx) = ins.ins.branch_dest().and_then(|dest| addr_map.get(&dest)) {
|
||||||
|
if let Some(branch) = branches.get_mut(ins_idx) {
|
||||||
|
ins_diff.branch_to =
|
||||||
|
Some(ObjInsBranchTo { ins_idx: *ins_idx, branch_idx: branch.branch_idx });
|
||||||
|
branch.ins_idx.push(i);
|
||||||
|
} else {
|
||||||
|
ins_diff.branch_to = Some(ObjInsBranchTo { ins_idx: *ins_idx, branch_idx });
|
||||||
|
branches.insert(*ins_idx, ObjInsBranchFrom { ins_idx: vec![i], branch_idx });
|
||||||
|
branch_idx += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Store branch from
|
||||||
|
for (i, branch) in branches {
|
||||||
|
vec[i].branch_from = Some(branch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reloc_eq(left_reloc: Option<&ObjReloc>, right_reloc: Option<&ObjReloc>) -> bool {
|
||||||
|
if let (Some(left), Some(right)) = (left_reloc, right_reloc) {
|
||||||
|
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 || left.target.address == right.target.address)
|
||||||
|
}
|
||||||
|
(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 {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn arg_eq(
|
||||||
|
left: &ObjInsArg,
|
||||||
|
right: &ObjInsArg,
|
||||||
|
left_diff: &ObjInsDiff,
|
||||||
|
right_diff: &ObjInsDiff,
|
||||||
|
) -> bool {
|
||||||
|
return match left {
|
||||||
|
ObjInsArg::Arg(l) => match right {
|
||||||
|
ObjInsArg::Arg(r) => match r {
|
||||||
|
Argument::BranchDest(_) => {
|
||||||
|
// Compare dest instruction idx after diffing
|
||||||
|
left_diff.branch_to.as_ref().map(|b| b.ins_idx)
|
||||||
|
== right_diff.branch_to.as_ref().map(|b| b.ins_idx)
|
||||||
|
}
|
||||||
|
_ => format!("{}", l) == format!("{}", r),
|
||||||
|
},
|
||||||
|
_ => false,
|
||||||
|
},
|
||||||
|
ObjInsArg::Reloc => {
|
||||||
|
matches!(right, ObjInsArg::Reloc)
|
||||||
|
&& reloc_eq(
|
||||||
|
left_diff.ins.as_ref().and_then(|i| i.reloc.as_ref()),
|
||||||
|
right_diff.ins.as_ref().and_then(|i| i.reloc.as_ref()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ObjInsArg::RelocOffset => {
|
||||||
|
matches!(right, ObjInsArg::RelocOffset)
|
||||||
|
&& reloc_eq(
|
||||||
|
left_diff.ins.as_ref().and_then(|i| i.reloc.as_ref()),
|
||||||
|
right_diff.ins.as_ref().and_then(|i| i.reloc.as_ref()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct InsDiffState {
|
||||||
|
diff_count: usize,
|
||||||
|
left_arg_idx: usize,
|
||||||
|
right_arg_idx: usize,
|
||||||
|
left_args_idx: BTreeMap<String, usize>,
|
||||||
|
right_args_idx: BTreeMap<String, usize>,
|
||||||
|
}
|
||||||
|
#[derive(Default)]
|
||||||
|
struct InsDiffResult {
|
||||||
|
kind: ObjInsDiffKind,
|
||||||
|
left_args_diff: Vec<Option<ObjInsArgDiff>>,
|
||||||
|
right_args_diff: Vec<Option<ObjInsArgDiff>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compare_ins(
|
||||||
|
left: &ObjInsDiff,
|
||||||
|
right: &ObjInsDiff,
|
||||||
|
state: &mut InsDiffState,
|
||||||
|
) -> Result<InsDiffResult> {
|
||||||
|
let mut result = InsDiffResult::default();
|
||||||
|
if let (Some(left_ins), Some(right_ins)) = (&left.ins, &right.ins) {
|
||||||
|
if left_ins.args.len() != right_ins.args.len() || left_ins.ins.op != right_ins.ins.op {
|
||||||
|
// Totally different op
|
||||||
|
result.kind = ObjInsDiffKind::Replace;
|
||||||
|
state.diff_count += 1;
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
if left_ins.mnemonic != right_ins.mnemonic {
|
||||||
|
// Same op but different mnemonic, still cmp args
|
||||||
|
result.kind = ObjInsDiffKind::OpMismatch;
|
||||||
|
state.diff_count += 1;
|
||||||
|
}
|
||||||
|
for (a, b) in left_ins.args.iter().zip(&right_ins.args) {
|
||||||
|
if arg_eq(a, b, left, right) {
|
||||||
|
result.left_args_diff.push(None);
|
||||||
|
result.right_args_diff.push(None);
|
||||||
|
} else {
|
||||||
|
if result.kind == ObjInsDiffKind::None {
|
||||||
|
result.kind = ObjInsDiffKind::ArgMismatch;
|
||||||
|
state.diff_count += 1;
|
||||||
|
}
|
||||||
|
let a_str = match a {
|
||||||
|
ObjInsArg::Arg(arg) => format!("{}", arg),
|
||||||
|
ObjInsArg::Reloc | ObjInsArg::RelocOffset => String::new(),
|
||||||
|
};
|
||||||
|
let a_diff = if let Some(idx) = state.left_args_idx.get(&a_str) {
|
||||||
|
ObjInsArgDiff { idx: *idx }
|
||||||
|
} else {
|
||||||
|
let idx = state.left_arg_idx;
|
||||||
|
state.left_args_idx.insert(a_str, idx);
|
||||||
|
state.left_arg_idx += 1;
|
||||||
|
ObjInsArgDiff { idx }
|
||||||
|
};
|
||||||
|
let b_str = match b {
|
||||||
|
ObjInsArg::Arg(arg) => format!("{}", arg),
|
||||||
|
ObjInsArg::Reloc | ObjInsArg::RelocOffset => String::new(),
|
||||||
|
};
|
||||||
|
let b_diff = if let Some(idx) = state.right_args_idx.get(&b_str) {
|
||||||
|
ObjInsArgDiff { idx: *idx }
|
||||||
|
} else {
|
||||||
|
let idx = state.right_arg_idx;
|
||||||
|
state.right_args_idx.insert(b_str, idx);
|
||||||
|
state.right_arg_idx += 1;
|
||||||
|
ObjInsArgDiff { idx }
|
||||||
|
};
|
||||||
|
result.left_args_diff.push(Some(a_diff));
|
||||||
|
result.right_args_diff.push(Some(b_diff));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if left.ins.is_some() {
|
||||||
|
result.kind = ObjInsDiffKind::Delete;
|
||||||
|
state.diff_count += 1;
|
||||||
|
} else {
|
||||||
|
result.kind = ObjInsDiffKind::Insert;
|
||||||
|
state.diff_count += 1;
|
||||||
|
}
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_section<'a>(obj: &'a mut ObjInfo, name: &str) -> Option<&'a mut ObjSection> {
|
||||||
|
obj.sections.iter_mut().find(|s| s.name == name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_symbol<'a>(symbols: &'a mut [ObjSymbol], name: &str) -> Option<&'a mut ObjSymbol> {
|
||||||
|
symbols.iter_mut().find(|s| s.name == name)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn diff_objs(left: &mut ObjInfo, right: &mut ObjInfo) -> Result<()> {
|
||||||
|
for left_section in &mut left.sections {
|
||||||
|
if let Some(right_section) = find_section(right, &left_section.name) {
|
||||||
|
for left_symbol in &mut left_section.symbols {
|
||||||
|
if let Some(right_symbol) =
|
||||||
|
find_symbol(&mut right_section.symbols, &left_symbol.name)
|
||||||
|
{
|
||||||
|
left_symbol.diff_symbol = Some(right_symbol.name.clone());
|
||||||
|
right_symbol.diff_symbol = Some(left_symbol.name.clone());
|
||||||
|
if left_section.kind == ObjSectionKind::Code {
|
||||||
|
diff_code(
|
||||||
|
&left_section.data,
|
||||||
|
&right_section.data,
|
||||||
|
left_symbol,
|
||||||
|
right_symbol,
|
||||||
|
&left_section.relocations,
|
||||||
|
&right_section.relocations,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -0,0 +1,253 @@
|
||||||
|
/// Adapted from https://crates.io/crates/rapidfuzz
|
||||||
|
// Copyright 2020 maxbachmann
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any
|
||||||
|
// person obtaining a copy of this software and associated
|
||||||
|
// documentation files (the "Software"), to deal in the
|
||||||
|
// Software without restriction, including without
|
||||||
|
// limitation the rights to use, copy, modify, merge,
|
||||||
|
// publish, distribute, sublicense, and/or sell copies of
|
||||||
|
// the Software, and to permit persons to whom the Software
|
||||||
|
// is furnished to do so, subject to the following
|
||||||
|
// conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice
|
||||||
|
// shall be included in all copies or substantial portions
|
||||||
|
// of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||||
|
// ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||||
|
// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||||
|
// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||||
|
// SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||||
|
// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
// DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
|
||||||
|
pub enum LevEditType {
|
||||||
|
Keep,
|
||||||
|
Replace,
|
||||||
|
Insert,
|
||||||
|
Delete,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
pub struct LevEditOp {
|
||||||
|
pub op_type: LevEditType, /* editing operation type */
|
||||||
|
pub first_start: usize, /* source block 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(query: &[u8], choice: &[u8]) -> Vec<LevEditOp> {
|
||||||
|
let string_affix = Affix::find(query, choice);
|
||||||
|
|
||||||
|
let first_string_len = string_affix.first_string_len;
|
||||||
|
let second_string_len = string_affix.second_string_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_rows = second_string_len + 1;
|
||||||
|
|
||||||
|
// TODO maybe use an actual matrix for readability
|
||||||
|
let mut cache_matrix: Vec<usize> = vec![0; matrix_rows * matrix_columns];
|
||||||
|
for (i, elem) in cache_matrix.iter_mut().enumerate().take(matrix_rows) {
|
||||||
|
*elem = i;
|
||||||
|
}
|
||||||
|
for i in 1..matrix_columns {
|
||||||
|
cache_matrix[matrix_rows * i] = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i, char1) in first_string.iter().enumerate() {
|
||||||
|
let mut prev = i * matrix_rows;
|
||||||
|
let current = prev + matrix_rows;
|
||||||
|
let mut x = i + 1;
|
||||||
|
for (p, char2p) in second_string.iter().enumerate() {
|
||||||
|
let mut c3 = cache_matrix[prev] + (char1 != char2p) as usize;
|
||||||
|
prev += 1;
|
||||||
|
x += 1;
|
||||||
|
if x >= c3 {
|
||||||
|
x = c3;
|
||||||
|
}
|
||||||
|
c3 = cache_matrix[prev] + 1;
|
||||||
|
if x > c3 {
|
||||||
|
x = c3;
|
||||||
|
}
|
||||||
|
cache_matrix[current + 1 + p] = x;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
editops_from_cost_matrix(
|
||||||
|
first_string,
|
||||||
|
second_string,
|
||||||
|
matrix_columns,
|
||||||
|
matrix_rows,
|
||||||
|
prefix_len,
|
||||||
|
cache_matrix,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn editops_from_cost_matrix(
|
||||||
|
string1: &[u8],
|
||||||
|
string2: &[u8],
|
||||||
|
len1: usize,
|
||||||
|
len2: usize,
|
||||||
|
prefix_len: usize,
|
||||||
|
cache_matrix: Vec<usize>,
|
||||||
|
) -> Vec<LevEditOp> {
|
||||||
|
let mut dir = 0;
|
||||||
|
|
||||||
|
let mut ops: Vec<LevEditOp> = vec![];
|
||||||
|
ops.reserve(cache_matrix[len1 * len2 - 1]);
|
||||||
|
|
||||||
|
let mut i = len1 - 1;
|
||||||
|
let mut j = 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
|
||||||
|
while i > 0 || j > 0 {
|
||||||
|
let current_value = cache_matrix[p];
|
||||||
|
|
||||||
|
let op_type;
|
||||||
|
|
||||||
|
if dir == -1 && j > 0 && current_value == cache_matrix[p - 1] + 1 {
|
||||||
|
op_type = LevEditType::Insert;
|
||||||
|
} else if dir == 1 && i > 0 && current_value == cache_matrix[p - len2] + 1 {
|
||||||
|
op_type = LevEditType::Delete;
|
||||||
|
} else if i > 0
|
||||||
|
&& j > 0
|
||||||
|
&& current_value == cache_matrix[p - len2 - 1]
|
||||||
|
&& string1[i - 1] == string2[j - 1]
|
||||||
|
{
|
||||||
|
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 =
|
||||||
|
LevEditOp { op_type, first_start: i + prefix_len, second_start: j + prefix_len };
|
||||||
|
ops.insert(0, edit_op);
|
||||||
|
}
|
||||||
|
|
||||||
|
ops
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Affix {
|
||||||
|
pub prefix_len: usize,
|
||||||
|
pub first_string_len: usize,
|
||||||
|
pub second_string_len: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Affix {
|
||||||
|
pub fn find(first_string: &[u8], second_string: &[u8]) -> Affix {
|
||||||
|
// remove common prefix and suffix (linear vs square runtime for levensthein)
|
||||||
|
let mut first_iter = first_string.iter();
|
||||||
|
let mut second_iter = second_string.iter();
|
||||||
|
|
||||||
|
let mut limit_start = 0;
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,242 @@
|
||||||
|
use std::{fs, path::Path};
|
||||||
|
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use cwdemangle::demangle;
|
||||||
|
use flagset::Flags;
|
||||||
|
use object::{
|
||||||
|
Object, ObjectSection, ObjectSymbol, RelocationKind, RelocationTarget, SectionKind, SymbolKind,
|
||||||
|
SymbolSection,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::obj::{
|
||||||
|
ObjInfo, ObjReloc, ObjRelocKind, ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlagSet,
|
||||||
|
ObjSymbolFlags,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn to_obj_section_kind(kind: SectionKind) -> ObjSectionKind {
|
||||||
|
match kind {
|
||||||
|
SectionKind::Text => ObjSectionKind::Code,
|
||||||
|
SectionKind::Data | SectionKind::ReadOnlyData => ObjSectionKind::Data,
|
||||||
|
SectionKind::UninitializedData => ObjSectionKind::Bss,
|
||||||
|
_ => panic!("Unhandled section kind {:?}", kind),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_obj_symbol(symbol: &object::Symbol<'_, '_>) -> Result<ObjSymbol> {
|
||||||
|
let mut name = symbol.name().context("Failed to process symbol name")?;
|
||||||
|
if name.is_empty() {
|
||||||
|
println!("Found empty sym: {:?}", symbol);
|
||||||
|
name = "?";
|
||||||
|
}
|
||||||
|
let mut flags = ObjSymbolFlagSet(ObjSymbolFlags::none());
|
||||||
|
if symbol.is_global() {
|
||||||
|
flags = ObjSymbolFlagSet(flags.0 | ObjSymbolFlags::Global);
|
||||||
|
}
|
||||||
|
if symbol.is_local() {
|
||||||
|
flags = ObjSymbolFlagSet(flags.0 | ObjSymbolFlags::Local);
|
||||||
|
}
|
||||||
|
if symbol.is_common() {
|
||||||
|
flags = ObjSymbolFlagSet(flags.0 | ObjSymbolFlags::Common);
|
||||||
|
}
|
||||||
|
if symbol.is_weak() {
|
||||||
|
flags = ObjSymbolFlagSet(flags.0 | ObjSymbolFlags::Weak);
|
||||||
|
}
|
||||||
|
Ok(ObjSymbol {
|
||||||
|
name: name.to_string(),
|
||||||
|
demangled_name: demangle(name),
|
||||||
|
address: symbol.address(),
|
||||||
|
size: symbol.size(),
|
||||||
|
size_known: symbol.size() != 0,
|
||||||
|
flags,
|
||||||
|
diff_symbol: None,
|
||||||
|
instructions: vec![],
|
||||||
|
match_percent: 0.0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const R_PPC_ADDR16_LO: u32 = 4;
|
||||||
|
const R_PPC_ADDR16_HI: u32 = 5;
|
||||||
|
const R_PPC_ADDR16_HA: u32 = 6;
|
||||||
|
const R_PPC_REL24: u32 = 10;
|
||||||
|
const R_PPC_REL14: u32 = 11;
|
||||||
|
const R_PPC_EMB_SDA21: u32 = 109;
|
||||||
|
|
||||||
|
fn filter_sections(obj_file: &object::File<'_>) -> Result<Vec<ObjSection>> {
|
||||||
|
let mut result = Vec::<ObjSection>::new();
|
||||||
|
for section in obj_file.sections() {
|
||||||
|
if section.size() == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if section.kind() != SectionKind::Text
|
||||||
|
&& section.kind() != SectionKind::Data
|
||||||
|
&& section.kind() != SectionKind::ReadOnlyData
|
||||||
|
&& section.kind() != SectionKind::UninitializedData
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let name = section.name().context("Failed to process section name")?;
|
||||||
|
let data = section.data().context("Failed to read section data")?;
|
||||||
|
result.push(ObjSection {
|
||||||
|
name: name.to_string(),
|
||||||
|
kind: to_obj_section_kind(section.kind()),
|
||||||
|
address: section.address(),
|
||||||
|
size: section.size(),
|
||||||
|
data: data.to_vec(),
|
||||||
|
index: section.index().0,
|
||||||
|
symbols: Vec::new(),
|
||||||
|
relocations: Vec::new(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
result.sort_by(|a, b| a.name.cmp(&b.name));
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn symbols_by_section(obj_file: &object::File<'_>, section: &ObjSection) -> Result<Vec<ObjSymbol>> {
|
||||||
|
let mut result = Vec::<ObjSymbol>::new();
|
||||||
|
for symbol in obj_file.symbols() {
|
||||||
|
if symbol.kind() == SymbolKind::Section {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(index) = symbol.section().index() {
|
||||||
|
if index.0 == section.index {
|
||||||
|
if symbol.is_local() && section.kind == ObjSectionKind::Code {
|
||||||
|
// TODO strip local syms in diff?
|
||||||
|
let name = symbol.name().context("Failed to process symbol name")?;
|
||||||
|
if name.starts_with("lbl_") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.push(to_obj_symbol(&symbol)?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.sort_by_key(|v| v.address);
|
||||||
|
let mut iter = result.iter_mut().peekable();
|
||||||
|
while let Some(symbol) = iter.next() {
|
||||||
|
if symbol.size == 0 {
|
||||||
|
if let Some(next_symbol) = iter.peek() {
|
||||||
|
symbol.size = next_symbol.address - symbol.address;
|
||||||
|
} else {
|
||||||
|
symbol.size = (section.address + section.size) - symbol.address;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn common_symbols(obj_file: &object::File<'_>) -> Result<Vec<ObjSymbol>> {
|
||||||
|
let mut result = Vec::<ObjSymbol>::new();
|
||||||
|
for symbol in obj_file.symbols() {
|
||||||
|
if symbol.is_common() {
|
||||||
|
result.push(to_obj_symbol(&symbol)?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn locate_section_symbol(
|
||||||
|
obj_file: &object::File<'_>,
|
||||||
|
target: &object::Symbol<'_, '_>,
|
||||||
|
address: u64,
|
||||||
|
) -> Result<ObjSymbol> {
|
||||||
|
let section_index =
|
||||||
|
target.section_index().ok_or_else(|| anyhow::Error::msg("Unknown section index"))?;
|
||||||
|
for symbol in obj_file.symbols() {
|
||||||
|
if !matches!(symbol.section_index(), Some(idx) if idx == section_index) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if symbol.kind() == SymbolKind::Section || symbol.address() != address {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return to_obj_symbol(&symbol);
|
||||||
|
}
|
||||||
|
Err(anyhow::Error::msg("Failed to locate reloc offset sym"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn relocations_by_section(
|
||||||
|
obj_file: &object::File<'_>,
|
||||||
|
section: &ObjSection,
|
||||||
|
) -> Result<Vec<ObjReloc>> {
|
||||||
|
let obj_section = obj_file
|
||||||
|
.section_by_name(§ion.name)
|
||||||
|
.ok_or_else(|| anyhow::Error::msg("Failed to locate section"))?;
|
||||||
|
let mut relocations = Vec::<ObjReloc>::new();
|
||||||
|
for (address, reloc) in obj_section.relocations() {
|
||||||
|
let symbol = match reloc.target() {
|
||||||
|
RelocationTarget::Symbol(idx) => obj_file
|
||||||
|
.symbol_by_index(idx)
|
||||||
|
.context("Failed to locate relocation target symbol")?,
|
||||||
|
_ => {
|
||||||
|
return Err(anyhow::Error::msg(format!(
|
||||||
|
"Unhandled relocation target: {:?}",
|
||||||
|
reloc.target()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let kind = match reloc.kind() {
|
||||||
|
RelocationKind::Absolute => ObjRelocKind::Absolute,
|
||||||
|
RelocationKind::Elf(kind) => match kind {
|
||||||
|
R_PPC_ADDR16_LO => ObjRelocKind::PpcAddr16Lo,
|
||||||
|
R_PPC_ADDR16_HI => ObjRelocKind::PpcAddr16Hi,
|
||||||
|
R_PPC_ADDR16_HA => ObjRelocKind::PpcAddr16Ha,
|
||||||
|
R_PPC_REL24 => ObjRelocKind::PpcRel24,
|
||||||
|
R_PPC_REL14 => ObjRelocKind::PpcRel14,
|
||||||
|
R_PPC_EMB_SDA21 => ObjRelocKind::PpcEmbSda21,
|
||||||
|
_ => {
|
||||||
|
return Err(anyhow::Error::msg(format!(
|
||||||
|
"Unhandled ELF relocation type: {}",
|
||||||
|
kind
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
return Err(anyhow::Error::msg(format!(
|
||||||
|
"Unhandled relocation type: {:?}",
|
||||||
|
reloc.kind()
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let target_section = match symbol.section() {
|
||||||
|
SymbolSection::Common => Some(".comm".to_string()),
|
||||||
|
SymbolSection::Section(idx) => obj_file
|
||||||
|
.section_by_index(idx)
|
||||||
|
.map(|s| s.name().map(|s| s.to_string()).ok())
|
||||||
|
.ok()
|
||||||
|
.flatten(),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
// println!("Reloc: {:?}", reloc.addend());
|
||||||
|
let target = match symbol.kind() {
|
||||||
|
SymbolKind::Text | SymbolKind::Data | SymbolKind::Unknown => to_obj_symbol(&symbol),
|
||||||
|
SymbolKind::Section => {
|
||||||
|
let addend = reloc.addend();
|
||||||
|
if addend < 0 {
|
||||||
|
Err(anyhow::Error::msg(format!("Negative addend in section reloc: {}", addend)))
|
||||||
|
} else {
|
||||||
|
locate_section_symbol(obj_file, &symbol, addend as u64)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => Err(anyhow::Error::msg(format!(
|
||||||
|
"Unhandled relocation symbol type {:?}",
|
||||||
|
symbol.kind()
|
||||||
|
))),
|
||||||
|
}?;
|
||||||
|
relocations.push(ObjReloc { kind, address, target, target_section });
|
||||||
|
}
|
||||||
|
Ok(relocations)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read(obj_path: &Path) -> Result<ObjInfo> {
|
||||||
|
let bin_data = fs::read(obj_path)?;
|
||||||
|
let obj_file = object::File::parse(&*bin_data)?;
|
||||||
|
let mut result = ObjInfo {
|
||||||
|
path: obj_path.to_owned(),
|
||||||
|
sections: filter_sections(&obj_file)?,
|
||||||
|
common: common_symbols(&obj_file)?,
|
||||||
|
};
|
||||||
|
for section in &mut result.sections {
|
||||||
|
section.symbols = symbols_by_section(&obj_file, section)?;
|
||||||
|
section.relocations = relocations_by_section(&obj_file, section)?;
|
||||||
|
}
|
||||||
|
Ok(result)
|
||||||
|
}
|
|
@ -0,0 +1,107 @@
|
||||||
|
use std::{
|
||||||
|
path::Path,
|
||||||
|
process::Command,
|
||||||
|
str::from_utf8,
|
||||||
|
sync::{mpsc::Receiver, Arc, RwLock},
|
||||||
|
};
|
||||||
|
|
||||||
|
use anyhow::{Context, Error, Result};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
app::AppConfig,
|
||||||
|
diff::diff_objs,
|
||||||
|
elf,
|
||||||
|
jobs::{queue_job, update_status, Job, JobResult, JobState, Status},
|
||||||
|
obj::ObjInfo,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct BuildStatus {
|
||||||
|
pub success: bool,
|
||||||
|
pub log: String,
|
||||||
|
}
|
||||||
|
pub struct BuildResult {
|
||||||
|
pub first_status: BuildStatus,
|
||||||
|
pub second_status: BuildStatus,
|
||||||
|
pub first_obj: Option<ObjInfo>,
|
||||||
|
pub second_obj: Option<ObjInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_make(cwd: &Path, arg: &Path) -> BuildStatus {
|
||||||
|
match (|| -> Result<BuildStatus> {
|
||||||
|
let output = Command::new("make")
|
||||||
|
.current_dir(cwd)
|
||||||
|
.arg(arg)
|
||||||
|
.output()
|
||||||
|
.context("Failed to execute build")?;
|
||||||
|
let stdout = from_utf8(&output.stdout).context("Failed to process stdout")?;
|
||||||
|
let stderr = from_utf8(&output.stderr).context("Failed to process stderr")?;
|
||||||
|
Ok(BuildStatus {
|
||||||
|
success: output.status.code().unwrap_or(-1) == 0,
|
||||||
|
log: format!("{}\n{}", stdout, stderr),
|
||||||
|
})
|
||||||
|
})() {
|
||||||
|
Ok(status) => status,
|
||||||
|
Err(e) => BuildStatus { success: false, log: e.to_string() },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_build(
|
||||||
|
status: &Status,
|
||||||
|
cancel: Receiver<()>,
|
||||||
|
obj_path: String,
|
||||||
|
config: Arc<RwLock<AppConfig>>,
|
||||||
|
) -> Result<Box<BuildResult>> {
|
||||||
|
let config = config.read().map_err(|_| Error::msg("Failed to lock app config"))?.clone();
|
||||||
|
let project_dir =
|
||||||
|
config.project_dir.as_ref().ok_or_else(|| Error::msg("Missing project dir"))?;
|
||||||
|
let mut asm_path = config
|
||||||
|
.build_asm_dir
|
||||||
|
.as_ref()
|
||||||
|
.ok_or_else(|| Error::msg("Missing build asm dir"))?
|
||||||
|
.to_owned();
|
||||||
|
asm_path.push(&obj_path);
|
||||||
|
let mut src_path = config
|
||||||
|
.build_src_dir
|
||||||
|
.as_ref()
|
||||||
|
.ok_or_else(|| Error::msg("Missing build src dir"))?
|
||||||
|
.to_owned();
|
||||||
|
src_path.push(&obj_path);
|
||||||
|
let asm_path_rel =
|
||||||
|
asm_path.strip_prefix(project_dir).context("Failed to create relative asm obj path")?;
|
||||||
|
let src_path_rel =
|
||||||
|
src_path.strip_prefix(project_dir).context("Failed to create relative src obj path")?;
|
||||||
|
|
||||||
|
update_status(status, format!("Building asm {}", obj_path), 0, 5, &cancel)?;
|
||||||
|
let first_status = run_make(project_dir, asm_path_rel);
|
||||||
|
|
||||||
|
update_status(status, format!("Building src {}", obj_path), 1, 5, &cancel)?;
|
||||||
|
let second_status = run_make(project_dir, src_path_rel);
|
||||||
|
|
||||||
|
let mut first_obj = if first_status.success {
|
||||||
|
update_status(status, format!("Loading asm {}", obj_path), 2, 5, &cancel)?;
|
||||||
|
Some(elf::read(&asm_path)?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut second_obj = if second_status.success {
|
||||||
|
update_status(status, format!("Loading src {}", obj_path), 3, 5, &cancel)?;
|
||||||
|
Some(elf::read(&src_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, 5, &cancel)?;
|
||||||
|
diff_objs(first_obj, second_obj)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
update_status(status, "Complete".to_string(), 5, 5, &cancel)?;
|
||||||
|
Ok(Box::new(BuildResult { first_status, second_status, first_obj, second_obj }))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn queue_build(obj_path: String, config: Arc<RwLock<AppConfig>>) -> JobState {
|
||||||
|
queue_job(Job::Build, move |status, cancel| {
|
||||||
|
run_build(status, cancel, obj_path, config).map(JobResult::Build)
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,104 @@
|
||||||
|
use std::{
|
||||||
|
sync::{
|
||||||
|
atomic::{AtomicUsize, Ordering},
|
||||||
|
mpsc::{Receiver, Sender, TryRecvError},
|
||||||
|
Arc, RwLock,
|
||||||
|
},
|
||||||
|
thread::JoinHandle,
|
||||||
|
};
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
use crate::jobs::build::BuildResult;
|
||||||
|
|
||||||
|
pub mod build;
|
||||||
|
|
||||||
|
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
|
||||||
|
pub enum Job {
|
||||||
|
Build,
|
||||||
|
}
|
||||||
|
pub static JOB_ID: AtomicUsize = AtomicUsize::new(0);
|
||||||
|
pub struct JobState {
|
||||||
|
pub id: usize,
|
||||||
|
pub job_type: Job,
|
||||||
|
pub handle: Option<JoinHandle<JobResult>>,
|
||||||
|
pub status: Arc<RwLock<JobStatus>>,
|
||||||
|
pub cancel: Sender<()>,
|
||||||
|
pub should_remove: bool,
|
||||||
|
}
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct JobStatus {
|
||||||
|
pub title: String,
|
||||||
|
pub progress_percent: f32,
|
||||||
|
pub progress_items: Option<[u32; 2]>,
|
||||||
|
pub status: String,
|
||||||
|
pub error: Option<anyhow::Error>,
|
||||||
|
}
|
||||||
|
pub enum JobResult {
|
||||||
|
None,
|
||||||
|
Build(Box<BuildResult>),
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_cancel(rx: &Receiver<()>) -> bool {
|
||||||
|
match rx.try_recv() {
|
||||||
|
Ok(_) | Err(TryRecvError::Disconnected) => true,
|
||||||
|
Err(_) => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Status = Arc<RwLock<JobStatus>>;
|
||||||
|
|
||||||
|
fn queue_job(
|
||||||
|
job_type: Job,
|
||||||
|
run: impl FnOnce(&Status, Receiver<()>) -> Result<JobResult> + Send + 'static,
|
||||||
|
) -> JobState {
|
||||||
|
let status = Arc::new(RwLock::new(JobStatus {
|
||||||
|
title: String::new(),
|
||||||
|
progress_percent: 0.0,
|
||||||
|
progress_items: None,
|
||||||
|
status: "".to_string(),
|
||||||
|
error: None,
|
||||||
|
}));
|
||||||
|
let status_clone = status.clone();
|
||||||
|
let (tx, rx) = std::sync::mpsc::channel();
|
||||||
|
let handle = std::thread::spawn(move || {
|
||||||
|
return match run(&status, rx) {
|
||||||
|
Ok(state) => state,
|
||||||
|
Err(e) => {
|
||||||
|
if let Ok(mut w) = status.write() {
|
||||||
|
w.error = Some(e);
|
||||||
|
}
|
||||||
|
JobResult::None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
let id = JOB_ID.fetch_add(1, Ordering::Relaxed);
|
||||||
|
log::info!("Started job {}", id);
|
||||||
|
JobState {
|
||||||
|
id,
|
||||||
|
job_type,
|
||||||
|
handle: Some(handle),
|
||||||
|
status: status_clone,
|
||||||
|
cancel: tx,
|
||||||
|
should_remove: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_status(
|
||||||
|
status: &Status,
|
||||||
|
str: String,
|
||||||
|
count: u32,
|
||||||
|
total: u32,
|
||||||
|
cancel: &Receiver<()>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let mut w = status.write().map_err(|_| anyhow::Error::msg("Failed to lock job status"))?;
|
||||||
|
w.progress_items = Some([count, total]);
|
||||||
|
w.progress_percent = count as f32 / total as f32;
|
||||||
|
if should_cancel(cancel) {
|
||||||
|
w.status = "Cancelled".to_string();
|
||||||
|
return Err(anyhow::Error::msg("Cancelled"));
|
||||||
|
} else {
|
||||||
|
w.status = str;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
#![warn(clippy::all, rust_2018_idioms)]
|
||||||
|
|
||||||
|
pub use app::App;
|
||||||
|
|
||||||
|
mod app;
|
||||||
|
mod diff;
|
||||||
|
mod editops;
|
||||||
|
mod elf;
|
||||||
|
mod jobs;
|
||||||
|
mod obj;
|
||||||
|
mod views;
|
|
@ -0,0 +1,31 @@
|
||||||
|
#![warn(clippy::all, rust_2018_idioms)]
|
||||||
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
|
||||||
|
|
||||||
|
// When compiling natively:
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
fn main() {
|
||||||
|
// Log to stdout (if you run with `RUST_LOG=debug`).
|
||||||
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
|
let native_options = eframe::NativeOptions::default();
|
||||||
|
// native_options.renderer = eframe::Renderer::Wgpu;
|
||||||
|
eframe::run_native("objdiff", native_options, Box::new(|cc| Box::new(objdiff::App::new(cc))));
|
||||||
|
}
|
||||||
|
|
||||||
|
// when compiling to web using trunk.
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
fn main() {
|
||||||
|
// Make sure panics are logged using `console.error`.
|
||||||
|
console_error_panic_hook::set_once();
|
||||||
|
|
||||||
|
// Redirect tracing to console.log and friends:
|
||||||
|
tracing_wasm::set_as_global_default();
|
||||||
|
|
||||||
|
let web_options = eframe::WebOptions::default();
|
||||||
|
eframe::start_web(
|
||||||
|
"the_canvas_id", // hardcode it
|
||||||
|
web_options,
|
||||||
|
Box::new(|cc| Box::new(eframe_template::TemplateApp::new(cc))),
|
||||||
|
)
|
||||||
|
.expect("failed to start eframe");
|
||||||
|
}
|
|
@ -0,0 +1,132 @@
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use flagset::{flags, FlagSet};
|
||||||
|
|
||||||
|
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
|
||||||
|
pub enum ObjSectionKind {
|
||||||
|
Code,
|
||||||
|
Data,
|
||||||
|
Bss,
|
||||||
|
}
|
||||||
|
flags! {
|
||||||
|
pub enum ObjSymbolFlags: u8 {
|
||||||
|
Global,
|
||||||
|
Local,
|
||||||
|
Weak,
|
||||||
|
Common,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[derive(Debug, Copy, Clone, Default)]
|
||||||
|
pub struct ObjSymbolFlagSet(pub(crate) FlagSet<ObjSymbolFlags>);
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ObjSection {
|
||||||
|
pub name: String,
|
||||||
|
pub kind: ObjSectionKind,
|
||||||
|
pub address: u64,
|
||||||
|
pub size: u64,
|
||||||
|
pub data: Vec<u8>,
|
||||||
|
pub index: usize,
|
||||||
|
pub symbols: Vec<ObjSymbol>,
|
||||||
|
pub relocations: Vec<ObjReloc>,
|
||||||
|
}
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum ObjInsArg {
|
||||||
|
Arg(ppc750cl::Argument),
|
||||||
|
Reloc,
|
||||||
|
RelocOffset,
|
||||||
|
}
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
pub struct ObjInsArgDiff {
|
||||||
|
/// Incrementing index for coloring
|
||||||
|
pub idx: usize,
|
||||||
|
}
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ObjInsBranchFrom {
|
||||||
|
/// Source instruction indices
|
||||||
|
pub ins_idx: Vec<usize>,
|
||||||
|
/// Incrementing index for coloring
|
||||||
|
pub branch_idx: usize,
|
||||||
|
}
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ObjInsBranchTo {
|
||||||
|
/// Target instruction index
|
||||||
|
pub ins_idx: usize,
|
||||||
|
/// Incrementing index for coloring
|
||||||
|
pub branch_idx: usize,
|
||||||
|
}
|
||||||
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
|
||||||
|
pub enum ObjInsDiffKind {
|
||||||
|
#[default]
|
||||||
|
None,
|
||||||
|
OpMismatch,
|
||||||
|
ArgMismatch,
|
||||||
|
Replace,
|
||||||
|
Delete,
|
||||||
|
Insert,
|
||||||
|
}
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ObjIns {
|
||||||
|
pub ins: ppc750cl::Ins,
|
||||||
|
pub mnemonic: String,
|
||||||
|
pub args: Vec<ObjInsArg>,
|
||||||
|
pub reloc: Option<ObjReloc>,
|
||||||
|
}
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct ObjInsDiff {
|
||||||
|
pub ins: Option<ObjIns>,
|
||||||
|
/// Diff kind
|
||||||
|
pub kind: ObjInsDiffKind,
|
||||||
|
/// Branches from instruction
|
||||||
|
pub branch_from: Option<ObjInsBranchFrom>,
|
||||||
|
/// Branches to instruction
|
||||||
|
pub branch_to: Option<ObjInsBranchTo>,
|
||||||
|
/// Arg diffs
|
||||||
|
pub arg_diff: Vec<Option<ObjInsArgDiff>>,
|
||||||
|
}
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ObjSymbol {
|
||||||
|
pub name: String,
|
||||||
|
pub demangled_name: Option<String>,
|
||||||
|
pub address: u64,
|
||||||
|
pub size: u64,
|
||||||
|
pub size_known: bool,
|
||||||
|
pub flags: ObjSymbolFlagSet,
|
||||||
|
|
||||||
|
// Diff
|
||||||
|
pub diff_symbol: Option<String>,
|
||||||
|
pub instructions: Vec<ObjInsDiff>,
|
||||||
|
pub match_percent: f32,
|
||||||
|
}
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ObjInfo {
|
||||||
|
pub path: PathBuf,
|
||||||
|
pub sections: Vec<ObjSection>,
|
||||||
|
pub common: Vec<ObjSymbol>,
|
||||||
|
}
|
||||||
|
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
|
||||||
|
pub enum ObjRelocKind {
|
||||||
|
Absolute,
|
||||||
|
PpcAddr16Hi,
|
||||||
|
PpcAddr16Ha,
|
||||||
|
PpcAddr16Lo,
|
||||||
|
// PpcAddr32,
|
||||||
|
// PpcRel32,
|
||||||
|
// PpcAddr24,
|
||||||
|
PpcRel24,
|
||||||
|
// PpcAddr14,
|
||||||
|
PpcRel14,
|
||||||
|
PpcEmbSda21,
|
||||||
|
}
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ObjReloc {
|
||||||
|
pub kind: ObjRelocKind,
|
||||||
|
pub address: u64,
|
||||||
|
pub target: ObjSymbol,
|
||||||
|
pub target_section: Option<String>,
|
||||||
|
}
|
||||||
|
// #[derive(Debug, Clone)]
|
||||||
|
// pub struct ObjInsDiff {
|
||||||
|
// pub kind: ObjInsDiffKind,
|
||||||
|
// pub left: Option<ObjIns>,
|
||||||
|
// pub right: Option<ObjIns>,
|
||||||
|
// }
|
|
@ -0,0 +1,87 @@
|
||||||
|
use std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
app::{AppConfig, ViewState},
|
||||||
|
jobs::build::queue_build,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn config_ui(ui: &mut egui::Ui, config: &Arc<RwLock<AppConfig>>, view_state: &mut ViewState) {
|
||||||
|
let mut config_guard = config.write().unwrap();
|
||||||
|
let AppConfig { project_dir, project_dir_change, build_asm_dir, build_src_dir, build_obj } =
|
||||||
|
&mut *config_guard;
|
||||||
|
|
||||||
|
if ui.button("Select project dir").clicked() {
|
||||||
|
if let Some(path) = rfd::FileDialog::new().pick_folder() {
|
||||||
|
*project_dir = Some(path);
|
||||||
|
*project_dir_change = true;
|
||||||
|
*build_asm_dir = None;
|
||||||
|
*build_src_dir = None;
|
||||||
|
*build_obj = 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 asm build dir").clicked() {
|
||||||
|
if let Some(path) = rfd::FileDialog::new().set_directory(&project_dir).pick_folder() {
|
||||||
|
*build_asm_dir = Some(path);
|
||||||
|
*build_obj = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(dir) = build_asm_dir {
|
||||||
|
ui.label(dir.to_string_lossy());
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
if ui.button("Select src build dir").clicked() {
|
||||||
|
if let Some(path) = rfd::FileDialog::new().set_directory(&project_dir).pick_folder() {
|
||||||
|
*build_src_dir = Some(path);
|
||||||
|
*build_obj = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(dir) = build_src_dir {
|
||||||
|
ui.label(dir.to_string_lossy());
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(build_src_dir) = build_src_dir {
|
||||||
|
if ui.button("Select obj").clicked() {
|
||||||
|
if let Some(path) = rfd::FileDialog::new()
|
||||||
|
.set_directory(&build_src_dir)
|
||||||
|
.add_filter("Object file", &["o"])
|
||||||
|
.pick_file()
|
||||||
|
{
|
||||||
|
let mut new_build_obj: Option<String> = None;
|
||||||
|
if let Ok(obj_path) = path.strip_prefix(&build_src_dir) {
|
||||||
|
new_build_obj = Some(obj_path.display().to_string());
|
||||||
|
} else if let Some(build_asm_dir) = build_asm_dir {
|
||||||
|
if let Ok(obj_path) = path.strip_prefix(&build_asm_dir) {
|
||||||
|
new_build_obj = Some(obj_path.display().to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(new_build_obj) = new_build_obj {
|
||||||
|
*build_obj = Some(new_build_obj.clone());
|
||||||
|
view_state.jobs.push(queue_build(new_build_obj, config.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(build_obj) = build_obj {
|
||||||
|
ui.label(&*build_obj);
|
||||||
|
if ui.button("Build").clicked() {
|
||||||
|
view_state.jobs.push(queue_build(build_obj.clone(), config.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.checkbox(&mut view_state.reverse_fn_order, "Reverse function order (deferred)");
|
||||||
|
ui.separator();
|
||||||
|
}
|
|
@ -0,0 +1,338 @@
|
||||||
|
use std::default::Default;
|
||||||
|
|
||||||
|
use cwdemangle::demangle;
|
||||||
|
use egui::{text::LayoutJob, Color32, FontFamily, FontId, Label, Sense, TextFormat};
|
||||||
|
use egui_extras::{Size, StripBuilder, TableBuilder};
|
||||||
|
use ppc750cl::Argument;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
app::ViewState,
|
||||||
|
obj::{
|
||||||
|
ObjInfo, ObjIns, ObjInsArg, ObjInsArgDiff, ObjInsDiff, ObjInsDiffKind, ObjReloc,
|
||||||
|
ObjRelocKind, ObjSymbol,
|
||||||
|
},
|
||||||
|
views::symbol_diff::match_color_for_symbol,
|
||||||
|
};
|
||||||
|
|
||||||
|
const FONT_SIZE: f32 = 14.0;
|
||||||
|
const FONT_ID: FontId = FontId::new(FONT_SIZE, FontFamily::Monospace);
|
||||||
|
|
||||||
|
const COLOR_RED: Color32 = Color32::from_rgb(200, 40, 41);
|
||||||
|
|
||||||
|
fn write_text(str: &str, color: Color32, job: &mut LayoutJob) {
|
||||||
|
job.append(str, 0.0, TextFormat { font_id: FONT_ID, color, ..Default::default() });
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_reloc(reloc: &ObjReloc, job: &mut LayoutJob) {
|
||||||
|
let name = reloc.target.demangled_name.as_ref().unwrap_or(&reloc.target.name);
|
||||||
|
write_text(name, Color32::LIGHT_GRAY, job);
|
||||||
|
match reloc.kind {
|
||||||
|
ObjRelocKind::PpcAddr16Lo => write_text("@l", Color32::GRAY, job),
|
||||||
|
ObjRelocKind::PpcAddr16Hi => write_text("@h", Color32::GRAY, job),
|
||||||
|
ObjRelocKind::PpcAddr16Ha => write_text("@ha", Color32::GRAY, job),
|
||||||
|
ObjRelocKind::PpcEmbSda21 => write_text("@sda21", Color32::GRAY, job),
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_ins(
|
||||||
|
ins: &ObjIns,
|
||||||
|
diff_kind: &ObjInsDiffKind,
|
||||||
|
args: &[Option<ObjInsArgDiff>],
|
||||||
|
base_addr: u32,
|
||||||
|
job: &mut LayoutJob,
|
||||||
|
) {
|
||||||
|
let base_color = match 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(
|
||||||
|
&ins.mnemonic,
|
||||||
|
match diff_kind {
|
||||||
|
ObjInsDiffKind::OpMismatch => Color32::LIGHT_BLUE,
|
||||||
|
_ => base_color,
|
||||||
|
},
|
||||||
|
job,
|
||||||
|
);
|
||||||
|
let mut writing_offset = false;
|
||||||
|
for (i, arg) in ins.args.iter().enumerate() {
|
||||||
|
if i == 0 {
|
||||||
|
write_text(" ", base_color, job);
|
||||||
|
}
|
||||||
|
if i > 0 && !writing_offset {
|
||||||
|
write_text(", ", base_color, job);
|
||||||
|
}
|
||||||
|
let color = if let Some(diff) = args.get(i).and_then(|a| a.as_ref()) {
|
||||||
|
COLOR_ROTATION[diff.idx % COLOR_ROTATION.len()]
|
||||||
|
} else {
|
||||||
|
base_color
|
||||||
|
};
|
||||||
|
match arg {
|
||||||
|
ObjInsArg::Arg(arg) => match arg {
|
||||||
|
Argument::Offset(val) => {
|
||||||
|
write_text(&format!("{}", val), color, job);
|
||||||
|
write_text("(", base_color, job);
|
||||||
|
writing_offset = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Argument::BranchDest(dest) => {
|
||||||
|
let addr = dest.0 + ins.ins.addr as i32 - base_addr as i32;
|
||||||
|
write_text(&format!("{:x}", addr), color, job);
|
||||||
|
}
|
||||||
|
Argument::Uimm(_) | Argument::Simm(_) => {
|
||||||
|
write_text(&format!("{}", arg), color, job);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
write_text(&format!("{}", arg), color, job);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ObjInsArg::Reloc => {
|
||||||
|
write_reloc(ins.reloc.as_ref().unwrap(), job);
|
||||||
|
}
|
||||||
|
ObjInsArg::RelocOffset => {
|
||||||
|
write_reloc(ins.reloc.as_ref().unwrap(), job);
|
||||||
|
write_text("(", base_color, job);
|
||||||
|
writing_offset = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if writing_offset {
|
||||||
|
write_text(")", base_color, job);
|
||||||
|
writing_offset = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ins_hover_ui(ui: &mut egui::Ui, ins: &ObjIns) {
|
||||||
|
ui.scope(|ui| {
|
||||||
|
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||||
|
ui.style_mut().wrap = Some(false);
|
||||||
|
|
||||||
|
ui.label(format!("{:02X?}", ins.ins.code.to_be_bytes()));
|
||||||
|
|
||||||
|
for arg in &ins.args {
|
||||||
|
if let ObjInsArg::Arg(arg) = arg {
|
||||||
|
match arg {
|
||||||
|
Argument::Uimm(v) => {
|
||||||
|
ui.label(format!("{} == {}", v, v.0));
|
||||||
|
}
|
||||||
|
Argument::Simm(v) => {
|
||||||
|
ui.label(format!("{} == {}", v, v.0));
|
||||||
|
}
|
||||||
|
Argument::Offset(v) => {
|
||||||
|
ui.label(format!("{} == {}", v, v.0));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(reloc) = &ins.reloc {
|
||||||
|
ui.label(format!("Relocation type: {:?}", reloc.kind));
|
||||||
|
ui.colored_label(Color32::WHITE, format!("Name: {}", reloc.target.name));
|
||||||
|
if let Some(section) = &reloc.target_section {
|
||||||
|
ui.colored_label(Color32::WHITE, format!("Section: {}", section));
|
||||||
|
ui.colored_label(Color32::WHITE, format!("Address: {:x}", reloc.target.address));
|
||||||
|
ui.colored_label(Color32::WHITE, format!("Size: {:x}", reloc.target.size));
|
||||||
|
} else {
|
||||||
|
ui.colored_label(Color32::WHITE, "Extern".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ins_context_menu(ui: &mut egui::Ui, ins: &ObjIns) {
|
||||||
|
ui.scope(|ui| {
|
||||||
|
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||||
|
ui.style_mut().wrap = Some(false);
|
||||||
|
|
||||||
|
// if ui.button("Copy hex").clicked() {}
|
||||||
|
|
||||||
|
for arg in &ins.args {
|
||||||
|
if let ObjInsArg::Arg(arg) = arg {
|
||||||
|
match arg {
|
||||||
|
Argument::Uimm(v) => {
|
||||||
|
if ui.button(format!("Copy \"{}\"", v)).clicked() {
|
||||||
|
ui.output().copied_text = format!("{}", v);
|
||||||
|
ui.close_menu();
|
||||||
|
}
|
||||||
|
if ui.button(format!("Copy \"{}\"", v.0)).clicked() {
|
||||||
|
ui.output().copied_text = format!("{}", v.0);
|
||||||
|
ui.close_menu();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Argument::Simm(v) => {
|
||||||
|
if ui.button(format!("Copy \"{}\"", v)).clicked() {
|
||||||
|
ui.output().copied_text = format!("{}", v);
|
||||||
|
ui.close_menu();
|
||||||
|
}
|
||||||
|
if ui.button(format!("Copy \"{}\"", v.0)).clicked() {
|
||||||
|
ui.output().copied_text = format!("{}", v.0);
|
||||||
|
ui.close_menu();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Argument::Offset(v) => {
|
||||||
|
if ui.button(format!("Copy \"{}\"", v)).clicked() {
|
||||||
|
ui.output().copied_text = format!("{}", v);
|
||||||
|
ui.close_menu();
|
||||||
|
}
|
||||||
|
if ui.button(format!("Copy \"{}\"", v.0)).clicked() {
|
||||||
|
ui.output().copied_text = format!("{}", v.0);
|
||||||
|
ui.close_menu();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(reloc) = &ins.reloc {
|
||||||
|
if let Some(name) = &reloc.target.demangled_name {
|
||||||
|
if ui.button(format!("Copy \"{}\"", name)).clicked() {
|
||||||
|
ui.output().copied_text = name.clone();
|
||||||
|
ui.close_menu();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ui.button(format!("Copy \"{}\"", reloc.target.name)).clicked() {
|
||||||
|
ui.output().copied_text = reloc.target.name.clone();
|
||||||
|
ui.close_menu();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const 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),
|
||||||
|
];
|
||||||
|
|
||||||
|
fn find_symbol<'a>(obj: &'a ObjInfo, section_name: &str, name: &str) -> Option<&'a ObjSymbol> {
|
||||||
|
let section = obj.sections.iter().find(|s| s.name == section_name)?;
|
||||||
|
section.symbols.iter().find(|s| s.name == name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn asm_row_ui(ui: &mut egui::Ui, ins_diff: &ObjInsDiff, symbol: &ObjSymbol) {
|
||||||
|
if ins_diff.kind != ObjInsDiffKind::None {
|
||||||
|
ui.painter().rect_filled(ui.available_rect_before_wrap(), 0.0, ui.visuals().faint_bg_color);
|
||||||
|
}
|
||||||
|
let mut job = LayoutJob::default();
|
||||||
|
if let Some(ins) = &ins_diff.ins {
|
||||||
|
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.ins.addr - symbol.address as u32)),
|
||||||
|
base_color,
|
||||||
|
&mut job,
|
||||||
|
);
|
||||||
|
if let Some(branch) = &ins_diff.branch_from {
|
||||||
|
write_text("~> ", COLOR_ROTATION[branch.branch_idx % COLOR_ROTATION.len()], &mut job);
|
||||||
|
} else {
|
||||||
|
write_text(" ", base_color, &mut job);
|
||||||
|
}
|
||||||
|
write_ins(ins, &ins_diff.kind, &ins_diff.arg_diff, symbol.address as u32, &mut job);
|
||||||
|
if let Some(branch) = &ins_diff.branch_to {
|
||||||
|
write_text(" ~>", COLOR_ROTATION[branch.branch_idx % COLOR_ROTATION.len()], &mut job);
|
||||||
|
}
|
||||||
|
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("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn asm_table_ui(
|
||||||
|
table: TableBuilder<'_>,
|
||||||
|
left_obj: &ObjInfo,
|
||||||
|
right_obj: &ObjInfo,
|
||||||
|
fn_name: &str,
|
||||||
|
) -> Option<()> {
|
||||||
|
let left_symbol = find_symbol(left_obj, ".text", fn_name)?;
|
||||||
|
let right_symbol = find_symbol(right_obj, ".text", fn_name)?;
|
||||||
|
table.body(|body| {
|
||||||
|
body.rows(FONT_SIZE, left_symbol.instructions.len(), |row_index, mut row| {
|
||||||
|
row.col(|ui| {
|
||||||
|
asm_row_ui(ui, &left_symbol.instructions[row_index], left_symbol);
|
||||||
|
});
|
||||||
|
row.col(|ui| {
|
||||||
|
asm_row_ui(ui, &right_symbol.instructions[row_index], right_symbol);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
Some(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn function_diff_ui(ui: &mut egui::Ui, view_state: &mut ViewState) {
|
||||||
|
if let (Some(result), Some(selected_symbol)) = (&view_state.build, &view_state.selected_symbol)
|
||||||
|
{
|
||||||
|
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| {
|
||||||
|
let demangled = demangle(selected_symbol);
|
||||||
|
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 asm:");
|
||||||
|
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(obj) = &result.second_obj {
|
||||||
|
if let Some(symbol) = find_symbol(obj, ".text", selected_symbol)
|
||||||
|
{
|
||||||
|
ui.colored_label(
|
||||||
|
match_color_for_symbol(symbol),
|
||||||
|
&format!("{:.0}%", symbol.match_percent),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ui.label("Diff src:");
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
use egui::{Color32, ProgressBar, Widget};
|
||||||
|
|
||||||
|
use crate::app::ViewState;
|
||||||
|
|
||||||
|
pub fn jobs_ui(ui: &mut egui::Ui, view_state: &ViewState) {
|
||||||
|
ui.label("Jobs");
|
||||||
|
|
||||||
|
for job in &view_state.jobs {
|
||||||
|
if let Ok(status) = job.status.read() {
|
||||||
|
ui.group(|ui| {
|
||||||
|
ui.label(&status.title);
|
||||||
|
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 {
|
||||||
|
format!("{:width$}", &status.status, width = STATUS_LENGTH)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
pub(crate) mod config;
|
||||||
|
pub(crate) mod function_diff;
|
||||||
|
pub(crate) mod jobs;
|
||||||
|
pub(crate) mod symbol_diff;
|
|
@ -0,0 +1,240 @@
|
||||||
|
use egui::{
|
||||||
|
text::LayoutJob, CollapsingHeader, Color32, FontFamily, FontId, Rgba, ScrollArea,
|
||||||
|
SelectableLabel, TextFormat, Ui, Widget,
|
||||||
|
};
|
||||||
|
use egui_extras::{Size, StripBuilder};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
app::{View, ViewState},
|
||||||
|
jobs::build::BuildStatus,
|
||||||
|
obj::{ObjInfo, ObjSymbol, ObjSymbolFlags},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn match_color_for_symbol(symbol: &ObjSymbol) -> Color32 {
|
||||||
|
if symbol.match_percent == 100.0 {
|
||||||
|
Color32::GREEN
|
||||||
|
} else if symbol.match_percent >= 50.0 {
|
||||||
|
Color32::LIGHT_BLUE
|
||||||
|
} else {
|
||||||
|
Color32::RED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn symbol_ui(
|
||||||
|
ui: &mut Ui,
|
||||||
|
symbol: &ObjSymbol,
|
||||||
|
highlighted_symbol: &mut Option<String>,
|
||||||
|
selected_symbol: &mut Option<String>,
|
||||||
|
current_view: &mut View,
|
||||||
|
) {
|
||||||
|
let mut job = LayoutJob::default();
|
||||||
|
let name: &str =
|
||||||
|
if let Some(demangled) = &symbol.demangled_name { demangled } else { &symbol.name };
|
||||||
|
let mut selected = false;
|
||||||
|
if let Some(sym) = highlighted_symbol {
|
||||||
|
selected = sym == &symbol.name;
|
||||||
|
}
|
||||||
|
let font_id = FontId::new(14.0, FontFamily::Monospace);
|
||||||
|
job.append("[", 0.0, TextFormat {
|
||||||
|
font_id: font_id.clone(),
|
||||||
|
color: Color32::GRAY,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
if symbol.flags.0.contains(ObjSymbolFlags::Common) {
|
||||||
|
job.append("c", 0.0, TextFormat {
|
||||||
|
font_id: font_id.clone(),
|
||||||
|
color: Color32::from_rgb(0, 255, 255),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
} else if symbol.flags.0.contains(ObjSymbolFlags::Global) {
|
||||||
|
job.append("g", 0.0, TextFormat {
|
||||||
|
font_id: font_id.clone(),
|
||||||
|
color: Color32::GREEN,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
} else if symbol.flags.0.contains(ObjSymbolFlags::Local) {
|
||||||
|
job.append("l", 0.0, TextFormat {
|
||||||
|
font_id: font_id.clone(),
|
||||||
|
color: Color32::GRAY,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if symbol.flags.0.contains(ObjSymbolFlags::Weak) {
|
||||||
|
job.append("w", 0.0, TextFormat {
|
||||||
|
font_id: font_id.clone(),
|
||||||
|
color: Color32::GRAY,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
job.append("] (", 0.0, TextFormat {
|
||||||
|
font_id: font_id.clone(),
|
||||||
|
color: Color32::GRAY,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
job.append(&format!("{:.0}%", symbol.match_percent), 0.0, TextFormat {
|
||||||
|
font_id: font_id.clone(),
|
||||||
|
color: match_color_for_symbol(symbol),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
job.append(") ", 0.0, TextFormat {
|
||||||
|
font_id: font_id.clone(),
|
||||||
|
color: Color32::GRAY,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
job.append(name, 0.0, TextFormat { font_id, color: Color32::WHITE, ..Default::default() });
|
||||||
|
let response = SelectableLabel::new(selected, job).ui(ui);
|
||||||
|
if response.clicked() {
|
||||||
|
*selected_symbol = Some(symbol.name.clone());
|
||||||
|
*current_view = View::FunctionDiff;
|
||||||
|
} else if response.hovered() {
|
||||||
|
*highlighted_symbol = Some(symbol.name.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn symbol_list_ui(
|
||||||
|
ui: &mut Ui,
|
||||||
|
obj: &ObjInfo,
|
||||||
|
highlighted_symbol: &mut Option<String>,
|
||||||
|
selected_symbol: &mut Option<String>,
|
||||||
|
current_view: &mut View,
|
||||||
|
reverse_function_order: bool,
|
||||||
|
) {
|
||||||
|
ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| {
|
||||||
|
ui.scope(|ui| {
|
||||||
|
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||||
|
ui.style_mut().wrap = Some(false);
|
||||||
|
|
||||||
|
if !obj.common.is_empty() {
|
||||||
|
CollapsingHeader::new(".comm").default_open(true).show(ui, |ui| {
|
||||||
|
for symbol in &obj.common {
|
||||||
|
symbol_ui(ui, symbol, highlighted_symbol, selected_symbol, current_view);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for section in &obj.sections {
|
||||||
|
CollapsingHeader::new(format!("{} ({:x})", section.name, section.size))
|
||||||
|
.default_open(true)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
if section.name == ".text" && reverse_function_order {
|
||||||
|
for symbol in section.symbols.iter().rev() {
|
||||||
|
symbol_ui(
|
||||||
|
ui,
|
||||||
|
symbol,
|
||||||
|
highlighted_symbol,
|
||||||
|
selected_symbol,
|
||||||
|
current_view,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for symbol in §ion.symbols {
|
||||||
|
symbol_ui(
|
||||||
|
ui,
|
||||||
|
symbol,
|
||||||
|
highlighted_symbol,
|
||||||
|
selected_symbol,
|
||||||
|
current_view,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_log_ui(ui: &mut Ui, status: &BuildStatus) {
|
||||||
|
ui.scope(|ui| {
|
||||||
|
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
||||||
|
ui.style_mut().wrap = Some(false);
|
||||||
|
ui.colored_label(Color32::from_rgb(255, 0, 0), &status.log);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn symbol_diff_ui(ui: &mut Ui, view_state: &mut ViewState) {
|
||||||
|
if let (Some(result), highlighted_symbol, selected_symbol, current_view) = (
|
||||||
|
&view_state.build,
|
||||||
|
&mut view_state.highlighted_symbol,
|
||||||
|
&mut view_state.selected_symbol,
|
||||||
|
&mut view_state.current_view,
|
||||||
|
) {
|
||||||
|
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 asm:");
|
||||||
|
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 src:");
|
||||||
|
if result.second_status.success {
|
||||||
|
ui.label("OK");
|
||||||
|
} else {
|
||||||
|
ui.colored_label(Rgba::from_rgb(1.0, 0.0, 0.0), "Fail");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ui.separator();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
strip.strip(|builder| {
|
||||||
|
builder.sizes(Size::remainder(), 2).horizontal(|mut strip| {
|
||||||
|
strip.cell(|ui| {
|
||||||
|
if result.first_status.success {
|
||||||
|
if let Some(obj) = &result.first_obj {
|
||||||
|
ui.push_id("left", |ui| {
|
||||||
|
symbol_list_ui(
|
||||||
|
ui,
|
||||||
|
obj,
|
||||||
|
highlighted_symbol,
|
||||||
|
selected_symbol,
|
||||||
|
current_view,
|
||||||
|
view_state.reverse_fn_order,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
build_log_ui(ui, &result.first_status);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
strip.cell(|ui| {
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
build_log_ui(ui, &result.second_status);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue