mirror of
https://github.com/encounter/objdiff.git
synced 2026-07-10 12:18:36 -07:00
Compare commits
86 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6bcac60df8 | |||
| b34108fa24 | |||
| 5f9e9063f8 | |||
| a3e11149f7 | |||
| d22c785610 | |||
| 5035f8ec36 | |||
| b8f401b0ba | |||
| 282783c081 | |||
| f5ae76e64d | |||
| dbabdbff11 | |||
| 2fdba8842e | |||
| 0fe4fadbc3 | |||
| 04be189e51 | |||
| 4d0a273d65 | |||
| 73917c12a8 | |||
| 2cd1e2e0ce | |||
| a6ddc506f8 | |||
| b8d7b3bc1a | |||
| 02daae417c | |||
| 8b1b4a9a79 | |||
| 13f1267eac | |||
| 3cebee6766 | |||
| ab0538e9a4 | |||
| b643265809 | |||
| 7a65e07e9b | |||
| 3c4092b965 | |||
| 0f599bea2b | |||
| 9927fc35b6 | |||
| 16fb6a491f | |||
| 2541aef71a | |||
| 7ad1f95bf7 | |||
| 66c879a95d | |||
| d53c358d2f | |||
| f24c017545 | |||
| 3b8603ab22 | |||
| 86b7d6b229 | |||
| 3f82781808 | |||
| 926dd06ce9 | |||
| de70b6fca0 | |||
| 79542a4725 | |||
| 0ebd2ae459 | |||
| 08ebea8bd2 | |||
| 46b8890159 | |||
| 8356fe670d | |||
| 7834185223 | |||
| 216dd70d5b | |||
| 976e629f08 | |||
| 67b28b7da1 | |||
| c0b9bfae21 | |||
| f3f4b0482a | |||
| 278683f103 | |||
| 6ad60de32a | |||
| 90c93164ab | |||
| c02eb31dbb | |||
| 86d92866aa | |||
| 51c3af2bbe | |||
| d0b8b449d9 | |||
| 32f5f202f7 | |||
| 481dbc185a | |||
| 26a4cc79cf | |||
| 5c96c2ee51 | |||
| d162fe847e | |||
| 2a24eb5aec | |||
| f7c291bd55 | |||
| 03a578c1bb | |||
| d09ef8e207 | |||
| 63552a58ae | |||
| 827f4a42bd | |||
| b2dcecc5d8 | |||
| 67b237eab6 | |||
| 66da80ff69 | |||
| 0a85f540f2 | |||
| d79b5b1233 | |||
| 8f5519cb6a | |||
| 19ec08be9a | |||
| 2ad0898efa | |||
| 03f2bcb8b1 | |||
| 781071761a | |||
| 572afa8551 | |||
| 90e81fad7e | |||
| 7a8efb4c88 | |||
| 56dac46280 | |||
| 1866158092 | |||
| fe8e7029f6 | |||
| e2c70342c9 | |||
| e6035b00df |
@@ -28,7 +28,7 @@ jobs:
|
||||
sudo apt-get update
|
||||
sudo apt-get -y install libgtk-3-dev
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- name: Setup Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
RUSTFLAGS: -D warnings
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- name: Setup Rust toolchain
|
||||
# We use nightly options in rustfmt.toml
|
||||
uses: dtolnay/rust-toolchain@nightly
|
||||
@@ -62,12 +62,12 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
checks:
|
||||
- advisories
|
||||
# - advisories
|
||||
- bans licenses sources
|
||||
# Prevent new advisories from failing CI
|
||||
continue-on-error: ${{ matrix.checks == 'advisories' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- uses: EmbarkStudios/cargo-deny-action@v2
|
||||
with:
|
||||
command: check ${{ matrix.checks }}
|
||||
@@ -86,7 +86,7 @@ jobs:
|
||||
sudo apt-get update
|
||||
sudo apt-get -y install libgtk-3-dev
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- name: Setup Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- name: Cache Rust workspace
|
||||
@@ -145,10 +145,10 @@ jobs:
|
||||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- name: Install uv
|
||||
if: matrix.build == 'zigbuild'
|
||||
uses: astral-sh/setup-uv@v6
|
||||
uses: astral-sh/setup-uv@v7
|
||||
- name: Install cargo-zigbuild
|
||||
if: matrix.build == 'zigbuild'
|
||||
run: |
|
||||
@@ -167,7 +167,7 @@ jobs:
|
||||
cargo ${{ matrix.build }} --profile ${{ env.BUILD_PROFILE }} --target ${{ matrix.target }}
|
||||
--bin ${{ env.CARGO_BIN_NAME }} --features ${{ matrix.features }}
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ env.CARGO_BIN_NAME }}-${{ matrix.name }}
|
||||
path: |
|
||||
@@ -213,10 +213,10 @@ jobs:
|
||||
sudo apt-get update
|
||||
sudo apt-get -y install ${{ matrix.packages }}
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- name: Install uv
|
||||
if: matrix.build == 'zigbuild'
|
||||
uses: astral-sh/setup-uv@v6
|
||||
uses: astral-sh/setup-uv@v7
|
||||
- name: Install cargo-zigbuild
|
||||
if: matrix.build == 'zigbuild'
|
||||
run: |
|
||||
@@ -235,7 +235,7 @@ jobs:
|
||||
cargo ${{ matrix.build }} --profile ${{ env.BUILD_PROFILE }} --target ${{ matrix.target }}
|
||||
--bin ${{ env.CARGO_BIN_NAME }} --features ${{ matrix.features }}
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ env.CARGO_BIN_NAME }}-${{ matrix.name }}
|
||||
path: |
|
||||
@@ -248,7 +248,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- name: Setup Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@nightly
|
||||
with:
|
||||
@@ -256,7 +256,7 @@ jobs:
|
||||
- name: Cache Rust workspace
|
||||
uses: Swatinem/rust-cache@v2
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6.4.0
|
||||
with:
|
||||
node-version: lts/*
|
||||
- name: Install dependencies
|
||||
@@ -264,7 +264,7 @@ jobs:
|
||||
- name: Build
|
||||
run: npm -C objdiff-wasm run build
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: wasm
|
||||
path: objdiff-wasm/dist/
|
||||
@@ -277,7 +277,7 @@ jobs:
|
||||
needs: [ build-cli, build-gui, build-wasm ]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- name: Check git tag against package versions
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -340,12 +340,11 @@ jobs:
|
||||
|
||||
release-cargo:
|
||||
name: Release (Cargo)
|
||||
if: 'false' # TODO re-enable when all dependencies are published
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ check-version ]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- name: Setup Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- name: Publish
|
||||
@@ -360,9 +359,9 @@ jobs:
|
||||
needs: [ check-version ]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v6.4.0
|
||||
with:
|
||||
node-version: lts/*
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
# Repository Guidelines
|
||||
|
||||
## Project Structure & Module Organization
|
||||
|
||||
- `objdiff-core`: core library with diffing logic and shared components
|
||||
- `objdiff-cli`: CLI/TUI with `ratatui`
|
||||
- `objdiff-gui`: GUI with `eframe`/`egui`
|
||||
- `objdiff-wasm`: WebAssembly bindings
|
||||
|
||||
objdiff has three main frontends: GUI, CLI/TUI, and [web](https://github.com/encounter/objdiff-web) (utilizing `objdiff-wasm`).
|
||||
|
||||
`objdiff-gui` is the most fully-featured and is the primary target for new features.
|
||||
|
||||
`objdiff-cli` has an interactive TUI `diff` mode (powered by `ratatui`) and a non-interactive `report` mode to generate detailed progress reports (e.g. for <https://decomp.dev>).
|
||||
|
||||
`objdiff-wasm` is compiled into a [WebAssembly Component](https://component-model.bytecodealliance.org/). The [API](objdiff-wasm/wit/objdiff.wit) is defined using [WIT](https://component-model.bytecodealliance.org/design/wit.html). The web interface is separate, but mirrors the GUI in functionality.
|
||||
|
||||
## Build, Test, and Development Commands
|
||||
|
||||
Run `cargo build` for a debug build and `cargo build --release` for an optimized build.
|
||||
The CLI can be exercised with `cargo run --release --bin objdiff-cli -- --help`.
|
||||
|
||||
The WASM build is not included in the workspace by default due to differences in toolchain and target; build it with `npm -C objdiff-wasm run build`. (nightly required)
|
||||
In general, do NOT use `--workspace`, as it will include the WASM crate and likely fail. Formatting and linting commands are exempt.
|
||||
|
||||
Pre-commit tasks:
|
||||
|
||||
- `cargo test` to run the test suite
|
||||
- `cargo +nightly fmt --all` to format code (nightly required)
|
||||
- `cargo +nightly clippy --all-targets --all-features --workspace -- -D warnings` to lint code (nightly required)
|
||||
- `cargo deny check` (`cargo install --locked cargo-deny` if needed) if dependencies change
|
||||
|
||||
## Testing Guidelines
|
||||
|
||||
Favor focused unit tests adjacent to the code, and integration scenarios in `objdiff-core/tests`.
|
||||
Integration tests utilize snapshots with the `insta` crate; run `curl -LsSf https://insta.rs/install.sh | sh` if needed.
|
||||
To generate updated snapshots, use `cargo insta test`, review diffs manually, then accept changes with `cargo insta accept` (or simply `mv` to accept specific snapshots).
|
||||
|
||||
## Configuration Tips
|
||||
|
||||
- `config.schema.json`: JSON schema for the user-facing project config file (`objdiff.json`)
|
||||
- `objdiff-core/config-schema.json`: Internal options schema to generate `DiffObjConfig` (see `objdiff-core/config_gen.rs`)
|
||||
|
||||
The project configuration (`objdiff.json`) is intended to be generated by the projects' build system. It includes paths to the target (expected) and base (current) object files, along with build commands and file watch patterns. See `README.md` for a summary of key options.
|
||||
Generated
+2168
-1295
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -14,7 +14,7 @@ default-members = [
|
||||
resolver = "3"
|
||||
|
||||
[workspace.package]
|
||||
version = "3.2.1"
|
||||
version = "3.7.3"
|
||||
authors = ["Luke Street <luke@street.dev>"]
|
||||
edition = "2024"
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
@@ -111,6 +111,25 @@
|
||||
"items": {
|
||||
"$ref": "#/$defs/progress_category"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"type": "object",
|
||||
"description": "Diff configuration options that should be applied automatically when the project is loaded.",
|
||||
"additionalProperties": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"examples": [
|
||||
{
|
||||
"demangler": "gnu_legacy"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
@@ -156,6 +175,20 @@
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"type": "object",
|
||||
"description": "Diff configuration options that should be applied when this unit is active.",
|
||||
"additionalProperties": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -75,6 +75,13 @@ ignore = [
|
||||
#{ crate = "a-crate-that-is-yanked@0.1.1", reason = "you can specify why you are ignoring the yanked crate" },
|
||||
{ id = "RUSTSEC-2024-0436", reason = "Unmaintained paste crate is an indirect dependency" },
|
||||
{ id = "RUSTSEC-2025-0052", reason = "Unmaintained async-std crate is an indirect dependency" },
|
||||
{ id = "RUSTSEC-2025-0134", reason = "Unmaintained rustls-pemfile crate is an indirect dependency" },
|
||||
{ id = "RUSTSEC-2026-0098", reason = "Build-time objdiff-wasm wit-deps dependency still uses reqwest 0.11; latest wit-deps has no safe upgrade path" },
|
||||
{ id = "RUSTSEC-2026-0099", reason = "Build-time objdiff-wasm wit-deps dependency still uses reqwest 0.11; latest wit-deps has no safe upgrade path" },
|
||||
{ id = "RUSTSEC-2026-0104", reason = "Build-time objdiff-wasm wit-deps dependency still uses reqwest 0.11; latest wit-deps has no safe upgrade path" },
|
||||
{ id = "RUSTSEC-2026-0192", reason = "Unmaintained ttf-parser is an egui 0.33 indirect dependency; egui 0.35 requires raising MSRV from 1.88 to 1.92" },
|
||||
{ id = "RUSTSEC-2026-0194", reason = "Build-time wayland-scanner dependency parses vendored protocol XML; no newer stable wayland-scanner release is available" },
|
||||
{ id = "RUSTSEC-2026-0195", reason = "Build-time wayland-scanner dependency parses vendored protocol XML; no newer stable wayland-scanner release is available" },
|
||||
]
|
||||
# If this is true, then cargo deny will use the git executable to fetch advisory database.
|
||||
# If this is false, then it uses a built-in git library.
|
||||
@@ -242,8 +249,6 @@ allow-git = []
|
||||
[sources.allow-org]
|
||||
# github.com organizations to allow git sources for
|
||||
github = [
|
||||
"encounter",
|
||||
"gimli-rs", # gimli
|
||||
]
|
||||
# gitlab.com organizations to allow git sources for
|
||||
gitlab = []
|
||||
|
||||
@@ -16,19 +16,19 @@ publish = false
|
||||
anyhow = "1.0"
|
||||
argp = "0.4"
|
||||
crossterm = "0.29"
|
||||
enable-ansi-support = "0.2"
|
||||
enable-ansi-support = "0.3"
|
||||
memmap2 = "0.9"
|
||||
objdiff-core = { path = "../objdiff-core", features = ["all"] }
|
||||
prost = "0.14"
|
||||
ratatui = "0.29"
|
||||
rayon = "1.10"
|
||||
ratatui = "0.30"
|
||||
rayon = "1.11"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
supports-color = "3.0"
|
||||
time = { version = "0.3", features = ["formatting", "local-offset"] }
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
typed-path = "0.11"
|
||||
typed-path = "0.12"
|
||||
|
||||
[target.'cfg(target_env = "musl")'.dependencies]
|
||||
mimalloc = "0.1"
|
||||
|
||||
+115
-44
@@ -1,4 +1,6 @@
|
||||
#![allow(unused)] // TODO
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
io::stdout,
|
||||
mem,
|
||||
sync::{
|
||||
@@ -19,15 +21,17 @@ use crossterm::{
|
||||
},
|
||||
};
|
||||
use objdiff_core::{
|
||||
bindings::diff::DiffResult,
|
||||
build::{
|
||||
BuildConfig, BuildStatus,
|
||||
watcher::{Watcher, create_watcher},
|
||||
},
|
||||
config::{
|
||||
ProjectConfig, ProjectObject, ProjectObjectMetadata, build_globset,
|
||||
ProjectConfig, ProjectObject, ProjectObjectMetadata, ProjectOptions, apply_project_options,
|
||||
build_globset,
|
||||
path::{check_path_buf, platform_path, platform_path_serde_option},
|
||||
},
|
||||
diff::{DiffObjConfig, MappingConfig, ObjectDiff},
|
||||
diff::{self, DiffObjConfig, DiffSide, MappingConfig, ObjectDiff},
|
||||
jobs::{
|
||||
Job, JobQueue, JobResult,
|
||||
objdiff::{ObjDiffConfig, start_build},
|
||||
@@ -39,7 +43,10 @@ use typed_path::{Utf8PlatformPath, Utf8PlatformPathBuf};
|
||||
|
||||
use crate::{
|
||||
cmd::apply_config_args,
|
||||
util::term::crossterm_panic_handler,
|
||||
util::{
|
||||
output::{OutputFormat, write_output},
|
||||
term::crossterm_panic_handler,
|
||||
},
|
||||
views::{EventControlFlow, EventResult, UiView, function_diff::FunctionDiffUi},
|
||||
};
|
||||
|
||||
@@ -59,29 +66,28 @@ pub struct Args {
|
||||
#[argp(option, short = 'u')]
|
||||
/// Unit name within project
|
||||
unit: Option<String>,
|
||||
#[argp(option, short = 'o', from_str_fn(platform_path))]
|
||||
/// Output file (one-shot mode) ("-" for stdout)
|
||||
output: Option<Utf8PlatformPathBuf>,
|
||||
#[argp(option)]
|
||||
/// Output format (json, json-pretty, proto) (default: json)
|
||||
format: Option<String>,
|
||||
#[argp(positional)]
|
||||
/// Function symbol to diff
|
||||
symbol: Option<String>,
|
||||
#[argp(option, short = 'c')]
|
||||
/// Configuration property (key=value)
|
||||
config: Vec<String>,
|
||||
#[argp(option, short = 'm')]
|
||||
/// Symbol mapping (target=base)
|
||||
mapping: Vec<String>,
|
||||
#[argp(option)]
|
||||
/// Left symbol name for selection
|
||||
selecting_left: Option<String>,
|
||||
#[argp(option)]
|
||||
/// Right symbol name for selection
|
||||
selecting_right: Option<String>,
|
||||
}
|
||||
|
||||
pub fn run(args: Args) -> Result<()> {
|
||||
let (target_path, base_path, project_config) =
|
||||
let (target_path, base_path, project_config, unit_options, symbol_mappings) =
|
||||
match (&args.target, &args.base, &args.project, &args.unit) {
|
||||
(Some(_), Some(_), None, None)
|
||||
| (Some(_), None, None, None)
|
||||
| (None, Some(_), None, None) => (args.target.clone(), args.base.clone(), None),
|
||||
| (None, Some(_), None, None) => {
|
||||
(args.target.clone(), args.base.clone(), None, None, BTreeMap::new())
|
||||
}
|
||||
(None, None, p, u) => {
|
||||
let project = match p {
|
||||
Some(project) => project.clone(),
|
||||
@@ -93,7 +99,7 @@ pub fn run(args: Args) -> Result<()> {
|
||||
let Some((project_config, project_config_info)) =
|
||||
objdiff_core::config::try_project_config(project.as_ref())
|
||||
else {
|
||||
bail!("Project config not found in {}", &project)
|
||||
bail!("Project config not found in {}", project)
|
||||
};
|
||||
let project_config = project_config.with_context(|| {
|
||||
format!("Reading project config {}", project_config_info.path.display())
|
||||
@@ -106,28 +112,32 @@ pub fn run(args: Args) -> Result<()> {
|
||||
.base_dir
|
||||
.as_ref()
|
||||
.map(|p| project.join(p.with_platform_encoding()));
|
||||
let objects = project_config
|
||||
.units
|
||||
let units = project_config.units.as_deref().unwrap_or_default();
|
||||
let objects = units
|
||||
.iter()
|
||||
.flatten()
|
||||
.map(|o| {
|
||||
ObjectConfig::new(
|
||||
o,
|
||||
&project,
|
||||
target_obj_dir.as_deref(),
|
||||
base_obj_dir.as_deref(),
|
||||
.enumerate()
|
||||
.map(|(idx, o)| {
|
||||
(
|
||||
ObjectConfig::new(
|
||||
o,
|
||||
&project,
|
||||
target_obj_dir.as_deref(),
|
||||
base_obj_dir.as_deref(),
|
||||
),
|
||||
idx,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let object = if let Some(u) = u {
|
||||
let (object, unit_idx) = if let Some(u) = u {
|
||||
objects
|
||||
.iter()
|
||||
.find(|obj| obj.name == *u)
|
||||
.find(|(obj, _)| obj.name == *u)
|
||||
.map(|(obj, idx)| (obj, *idx))
|
||||
.ok_or_else(|| anyhow!("Unit not found: {}", u))?
|
||||
} else if let Some(symbol_name) = &args.symbol {
|
||||
let mut idx = None;
|
||||
let mut count = 0usize;
|
||||
for (i, obj) in objects.iter().enumerate() {
|
||||
for (i, (obj, unit_idx)) in objects.iter().enumerate() {
|
||||
if obj
|
||||
.target_path
|
||||
.as_deref()
|
||||
@@ -135,7 +145,7 @@ pub fn run(args: Args) -> Result<()> {
|
||||
.transpose()?
|
||||
.unwrap_or(false)
|
||||
{
|
||||
idx = Some(i);
|
||||
idx = Some((i, *unit_idx));
|
||||
count += 1;
|
||||
if count > 1 {
|
||||
break;
|
||||
@@ -144,7 +154,7 @@ pub fn run(args: Args) -> Result<()> {
|
||||
}
|
||||
match (count, idx) {
|
||||
(0, None) => bail!("Symbol not found: {}", symbol_name),
|
||||
(1, Some(i)) => &objects[i],
|
||||
(1, Some((i, unit_idx))) => (&objects[i].0, unit_idx),
|
||||
(2.., Some(_)) => bail!(
|
||||
"Multiple instances of {} were found, try specifying a unit",
|
||||
symbol_name
|
||||
@@ -154,30 +164,82 @@ pub fn run(args: Args) -> Result<()> {
|
||||
} else {
|
||||
bail!("Must specify one of: symbol, project and unit, target and base objects")
|
||||
};
|
||||
let unit_options = units.get(unit_idx).and_then(|u| u.options().cloned());
|
||||
let target_path = object.target_path.clone();
|
||||
let base_path = object.base_path.clone();
|
||||
(target_path, base_path, Some(project_config))
|
||||
let symbol_mappings = object.symbol_mappings.clone();
|
||||
(target_path, base_path, Some(project_config), unit_options, symbol_mappings)
|
||||
}
|
||||
_ => bail!("Either target and base or project and unit must be specified"),
|
||||
};
|
||||
|
||||
run_interactive(args, target_path, base_path, project_config)
|
||||
if let Some(output) = &args.output {
|
||||
run_oneshot(
|
||||
&args,
|
||||
output,
|
||||
target_path.as_deref(),
|
||||
base_path.as_deref(),
|
||||
project_config.as_ref(),
|
||||
unit_options,
|
||||
&symbol_mappings,
|
||||
)
|
||||
} else {
|
||||
run_interactive(args, target_path, base_path, project_config, unit_options, symbol_mappings)
|
||||
}
|
||||
}
|
||||
|
||||
fn build_config_from_args(args: &Args) -> Result<(DiffObjConfig, MappingConfig)> {
|
||||
fn run_oneshot(
|
||||
args: &Args,
|
||||
output: &Utf8PlatformPath,
|
||||
target_path: Option<&Utf8PlatformPath>,
|
||||
base_path: Option<&Utf8PlatformPath>,
|
||||
project_config: Option<&ProjectConfig>,
|
||||
unit_options: Option<ProjectOptions>,
|
||||
symbol_mappings: &BTreeMap<String, String>,
|
||||
) -> Result<()> {
|
||||
let output_format = OutputFormat::from_option(args.format.as_deref())?;
|
||||
let (diff_config, mapping_config) =
|
||||
build_config_from_args(args, project_config, unit_options.as_ref(), symbol_mappings)?;
|
||||
let target = target_path
|
||||
.map(|p| {
|
||||
obj::read::read(p.as_ref(), &diff_config, DiffSide::Target)
|
||||
.with_context(|| format!("Loading {p}"))
|
||||
})
|
||||
.transpose()?;
|
||||
let base = base_path
|
||||
.map(|p| {
|
||||
obj::read::read(p.as_ref(), &diff_config, DiffSide::Base)
|
||||
.with_context(|| format!("Loading {p}"))
|
||||
})
|
||||
.transpose()?;
|
||||
let result =
|
||||
diff::diff_objs(target.as_ref(), base.as_ref(), None, &diff_config, &mapping_config)?;
|
||||
let left = target.as_ref().zip(result.left.as_ref());
|
||||
let right = base.as_ref().zip(result.right.as_ref());
|
||||
let diff_result = DiffResult::new(left, right, &diff_config)?;
|
||||
write_output(&diff_result, Some(output), output_format)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_config_from_args(
|
||||
args: &Args,
|
||||
project_config: Option<&ProjectConfig>,
|
||||
unit_options: Option<&ProjectOptions>,
|
||||
symbol_mappings: &BTreeMap<String, String>,
|
||||
) -> Result<(DiffObjConfig, MappingConfig)> {
|
||||
let mut diff_config = DiffObjConfig::default();
|
||||
apply_config_args(&mut diff_config, &args.config)?;
|
||||
let mut mapping_config = MappingConfig {
|
||||
mappings: Default::default(),
|
||||
selecting_left: args.selecting_left.clone(),
|
||||
selecting_right: args.selecting_right.clone(),
|
||||
};
|
||||
for mapping in &args.mapping {
|
||||
let (target, base) =
|
||||
mapping.split_once('=').context("--mapping expects \"target=base\"")?;
|
||||
mapping_config.mappings.insert(target.to_string(), base.to_string());
|
||||
if let Some(options) = project_config.and_then(|config| config.options.as_ref()) {
|
||||
apply_project_options(&mut diff_config, options)?;
|
||||
}
|
||||
Ok((diff_config, mapping_config))
|
||||
if let Some(options) = unit_options {
|
||||
apply_project_options(&mut diff_config, options)?;
|
||||
}
|
||||
apply_config_args(&mut diff_config, &args.config)?;
|
||||
Ok((diff_config, MappingConfig {
|
||||
mappings: symbol_mappings.clone(),
|
||||
selecting_left: None,
|
||||
selecting_right: None,
|
||||
}))
|
||||
}
|
||||
|
||||
pub struct AppState {
|
||||
@@ -238,6 +300,7 @@ pub struct ObjectConfig {
|
||||
pub base_path: Option<Utf8PlatformPathBuf>,
|
||||
pub metadata: ProjectObjectMetadata,
|
||||
pub complete: Option<bool>,
|
||||
pub symbol_mappings: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
impl ObjectConfig {
|
||||
@@ -267,6 +330,7 @@ impl ObjectConfig {
|
||||
base_path,
|
||||
metadata: object.metadata.clone().unwrap_or_default(),
|
||||
complete: object.complete(),
|
||||
symbol_mappings: object.symbol_mappings.clone().unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -316,11 +380,18 @@ fn run_interactive(
|
||||
target_path: Option<Utf8PlatformPathBuf>,
|
||||
base_path: Option<Utf8PlatformPathBuf>,
|
||||
project_config: Option<ProjectConfig>,
|
||||
unit_options: Option<ProjectOptions>,
|
||||
symbol_mappings: BTreeMap<String, String>,
|
||||
) -> Result<()> {
|
||||
let Some(symbol_name) = &args.symbol else { bail!("Interactive mode requires a symbol name") };
|
||||
let time_format = time::format_description::parse_borrowed::<2>("[hour]:[minute]:[second]")
|
||||
.context("Failed to parse time format")?;
|
||||
let (diff_obj_config, mapping_config) = build_config_from_args(&args)?;
|
||||
let (diff_obj_config, mapping_config) = build_config_from_args(
|
||||
&args,
|
||||
project_config.as_ref(),
|
||||
unit_options.as_ref(),
|
||||
&symbol_mappings,
|
||||
)?;
|
||||
let mut state = AppState {
|
||||
jobs: Default::default(),
|
||||
waker: Default::default(),
|
||||
|
||||
@@ -7,7 +7,7 @@ use objdiff_core::{
|
||||
ChangeItem, ChangeItemInfo, ChangeUnit, Changes, ChangesInput, Measures, REPORT_VERSION,
|
||||
Report, ReportCategory, ReportItem, ReportItemMetadata, ReportUnit, ReportUnitMetadata,
|
||||
},
|
||||
config::path::platform_path,
|
||||
config::{ProjectObject, ProjectOptions, apply_project_options, path::platform_path},
|
||||
diff,
|
||||
obj::{self, SectionKind, SymbolFlag, SymbolKind},
|
||||
};
|
||||
@@ -83,14 +83,13 @@ pub fn run(args: Args) -> Result<()> {
|
||||
}
|
||||
|
||||
fn generate(args: GenerateArgs) -> Result<()> {
|
||||
let mut diff_config = diff::DiffObjConfig {
|
||||
let base_diff_config = diff::DiffObjConfig {
|
||||
function_reloc_diffs: diff::FunctionRelocDiffs::None,
|
||||
combine_data_sections: true,
|
||||
combine_text_sections: true,
|
||||
ppc_calculate_pool_relocations: false,
|
||||
..Default::default()
|
||||
};
|
||||
apply_config_args(&mut diff_config, &args.config)?;
|
||||
|
||||
let output_format = OutputFormat::from_option(args.format.as_deref())?;
|
||||
let project_dir = args.project.as_deref().unwrap_or_else(|| Utf8PlatformPath::new("."));
|
||||
@@ -101,31 +100,44 @@ fn generate(args: GenerateArgs) -> Result<()> {
|
||||
Some((Err(err), _)) => bail!("Failed to load project configuration: {}", err),
|
||||
None => bail!("No project configuration found"),
|
||||
};
|
||||
info!(
|
||||
"Generating report for {} units (using {} threads)",
|
||||
project.units().len(),
|
||||
if args.deduplicate { 1 } else { rayon::current_num_threads() }
|
||||
);
|
||||
|
||||
let target_obj_dir =
|
||||
project.target_dir.as_ref().map(|p| project_dir.join(p.with_platform_encoding()));
|
||||
let base_obj_dir =
|
||||
project.base_dir.as_ref().map(|p| project_dir.join(p.with_platform_encoding()));
|
||||
let objects = project
|
||||
.units
|
||||
let project_units = project.units.as_deref().unwrap_or_default();
|
||||
let objects = project_units
|
||||
.iter()
|
||||
.flatten()
|
||||
.map(|o| {
|
||||
ObjectConfig::new(o, project_dir, target_obj_dir.as_deref(), base_obj_dir.as_deref())
|
||||
.enumerate()
|
||||
.map(|(idx, o)| {
|
||||
(
|
||||
ObjectConfig::new(
|
||||
o,
|
||||
project_dir,
|
||||
target_obj_dir.as_deref(),
|
||||
base_obj_dir.as_deref(),
|
||||
),
|
||||
idx,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
info!(
|
||||
"Generating report for {} units (using {} threads)",
|
||||
objects.len(),
|
||||
if args.deduplicate { 1 } else { rayon::current_num_threads() }
|
||||
);
|
||||
|
||||
let start = Instant::now();
|
||||
let mut units = vec![];
|
||||
let mut existing_functions: HashSet<String> = HashSet::new();
|
||||
if args.deduplicate {
|
||||
// If deduplicating, we need to run single-threaded
|
||||
for object in &objects {
|
||||
for (object, unit_idx) in &objects {
|
||||
let diff_config = build_unit_diff_config(
|
||||
&base_diff_config,
|
||||
project.options.as_ref(),
|
||||
project_units.get(*unit_idx).and_then(ProjectObject::options),
|
||||
&args.config,
|
||||
)?;
|
||||
if let Some(unit) = report_object(object, &diff_config, Some(&mut existing_functions))?
|
||||
{
|
||||
units.push(unit);
|
||||
@@ -134,7 +146,15 @@ fn generate(args: GenerateArgs) -> Result<()> {
|
||||
} else {
|
||||
let vec = objects
|
||||
.par_iter()
|
||||
.map(|object| report_object(object, &diff_config, None))
|
||||
.map(|(object, unit_idx)| {
|
||||
let diff_config = build_unit_diff_config(
|
||||
&base_diff_config,
|
||||
project.options.as_ref(),
|
||||
project_units.get(*unit_idx).and_then(ProjectObject::options),
|
||||
&args.config,
|
||||
)?;
|
||||
report_object(object, &diff_config, None)
|
||||
})
|
||||
.collect::<Result<Vec<Option<ReportUnit>>>>()?;
|
||||
units = vec.into_iter().flatten().collect();
|
||||
}
|
||||
@@ -156,6 +176,24 @@ fn generate(args: GenerateArgs) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_unit_diff_config(
|
||||
base: &diff::DiffObjConfig,
|
||||
project_options: Option<&ProjectOptions>,
|
||||
unit_options: Option<&ProjectOptions>,
|
||||
cli_args: &[String],
|
||||
) -> Result<diff::DiffObjConfig> {
|
||||
let mut diff_config = base.clone();
|
||||
if let Some(options) = project_options {
|
||||
apply_project_options(&mut diff_config, options)?;
|
||||
}
|
||||
if let Some(options) = unit_options {
|
||||
apply_project_options(&mut diff_config, options)?;
|
||||
}
|
||||
// CLI args override project and unit options
|
||||
apply_config_args(&mut diff_config, cli_args)?;
|
||||
Ok(diff_config)
|
||||
}
|
||||
|
||||
fn report_object(
|
||||
object: &ObjectConfig,
|
||||
diff_config: &diff::DiffObjConfig,
|
||||
@@ -172,7 +210,11 @@ fn report_object(
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
let mapping_config = diff::MappingConfig::default();
|
||||
let mapping_config = diff::MappingConfig {
|
||||
mappings: object.symbol_mappings.clone(),
|
||||
selecting_left: None,
|
||||
selecting_right: None,
|
||||
};
|
||||
let target = object
|
||||
.target_path
|
||||
.as_ref()
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#![allow(unused)] // TODO
|
||||
use core::cmp::Ordering;
|
||||
|
||||
use anyhow::Result;
|
||||
@@ -378,7 +379,6 @@ impl UiView for FunctionDiffUi {
|
||||
}
|
||||
// Reload
|
||||
KeyCode::Char('r') => {
|
||||
result.redraw = true;
|
||||
return EventControlFlow::Reload;
|
||||
}
|
||||
// Scroll right
|
||||
@@ -400,7 +400,6 @@ impl UiView for FunctionDiffUi {
|
||||
FunctionRelocDiffs::DataValue => FunctionRelocDiffs::All,
|
||||
FunctionRelocDiffs::All => FunctionRelocDiffs::None,
|
||||
};
|
||||
result.redraw = true;
|
||||
return EventControlFlow::Reload;
|
||||
}
|
||||
// Toggle three-way diff
|
||||
@@ -541,6 +540,7 @@ impl FunctionDiffUi {
|
||||
DiffText::Opcode(mnemonic, _op) => format!("{mnemonic} "),
|
||||
DiffText::Argument(arg) => arg.to_string(),
|
||||
DiffText::BranchDest(addr) => format!("{addr:x}"),
|
||||
DiffText::BranchArrow(_) => " ~> ".to_string(),
|
||||
DiffText::Symbol(sym) => {
|
||||
sym.demangled_name.as_ref().unwrap_or(&sym.name).clone()
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#![allow(unused)] // TODO
|
||||
use anyhow::Result;
|
||||
use crossterm::event::Event;
|
||||
use ratatui::Frame;
|
||||
|
||||
+31
-28
@@ -41,7 +41,8 @@ any-arch = [
|
||||
"dep:regex",
|
||||
"dep:similar",
|
||||
"dep:syn",
|
||||
"dep:encoding_rs"
|
||||
"dep:encoding_rs",
|
||||
"demangler",
|
||||
]
|
||||
bindings = [
|
||||
"dep:prost",
|
||||
@@ -88,38 +89,37 @@ std = [
|
||||
]
|
||||
mips = [
|
||||
"any-arch",
|
||||
"dep:cpp_demangle",
|
||||
"dep:cwdemangle",
|
||||
"dep:rabbitizer",
|
||||
]
|
||||
ppc = [
|
||||
"any-arch",
|
||||
"dep:cwdemangle",
|
||||
"dep:cwextab",
|
||||
"dep:powerpc",
|
||||
"dep:rlwinmdec",
|
||||
]
|
||||
x86 = [
|
||||
"any-arch",
|
||||
"dep:cpp_demangle",
|
||||
"dep:iced-x86",
|
||||
"dep:msvc-demangler",
|
||||
]
|
||||
arm = [
|
||||
"any-arch",
|
||||
"dep:arm-attr",
|
||||
"dep:cpp_demangle",
|
||||
"dep:unarm",
|
||||
]
|
||||
arm64 = [
|
||||
"any-arch",
|
||||
"dep:cpp_demangle",
|
||||
"dep:yaxpeax-arch",
|
||||
"dep:yaxpeax-arm",
|
||||
]
|
||||
superh = [
|
||||
"any-arch",
|
||||
]
|
||||
demangler = [
|
||||
"dep:cpp_demangle",
|
||||
"dep:cwdemangle",
|
||||
"dep:gnuv2_demangle",
|
||||
"dep:msvc-demangler",
|
||||
]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
features = ["all"]
|
||||
@@ -128,17 +128,17 @@ features = ["all"]
|
||||
anyhow = { version = "1.0", default-features = false }
|
||||
filetime = { version = "0.2", optional = true }
|
||||
flagset = { version = "0.4", default-features = false, optional = true }
|
||||
itertools = { version = "0.14", default-features = false, features = ["use_alloc"] }
|
||||
itertools = { version = "0.15", default-features = false, features = ["use_alloc"] }
|
||||
log = { version = "0.4", default-features = false, optional = true }
|
||||
memmap2 = { version = "0.9", optional = true }
|
||||
num-traits = { version = "0.2", default-features = false, optional = true }
|
||||
object = { version = "0.37", default-features = false, features = ["read_core", "elf", "coff"] }
|
||||
pbjson = { version = "0.8", default-features = false, optional = true }
|
||||
object = { version = "0.39.1", default-features = false, features = ["read_core", "elf", "coff"] }
|
||||
pbjson = { version = "0.9", default-features = false, optional = true }
|
||||
prost = { version = "0.14", default-features = false, features = ["derive"], optional = true }
|
||||
regex = { version = "1.11", default-features = false, features = [], optional = true }
|
||||
regex = { version = "1.12", default-features = false, features = [], optional = true }
|
||||
serde = { version = "1.0", default-features = false, features = ["derive"], optional = true }
|
||||
similar = { git = "https://github.com/encounter/similar.git", branch = "no_std", default-features = false, features = ["hashbrown"], optional = true }
|
||||
typed-path = { version = "0.11", default-features = false, optional = true }
|
||||
similar = { version = "3.1", default-features = false, features = ["hashbrown"], optional = true }
|
||||
typed-path = { version = "0.12", default-features = false, optional = true }
|
||||
|
||||
# config
|
||||
globset = { version = "0.4", default-features = false, optional = true }
|
||||
@@ -146,25 +146,22 @@ semver = { version = "1.0", default-features = false, optional = true }
|
||||
serde_json = { version = "1.0", default-features = false, features = ["alloc"], optional = true }
|
||||
|
||||
# dwarf
|
||||
gimli = { git = "https://github.com/gimli-rs/gimli", rev = "7335f00e7c39fd501511584fefb0ba974117c950", default-features = false, features = ["read"], optional = true }
|
||||
gimli = { version = "0.34.0", default-features = false, features = ["read"], optional = true }
|
||||
typed-arena = { version = "2.0", default-features = false, optional = true }
|
||||
|
||||
# ppc
|
||||
cwdemangle = { version = "1.0", optional = true }
|
||||
cwextab = { version = "1.1", optional = true }
|
||||
powerpc = { version = "0.4", optional = true }
|
||||
rlwinmdec = { version = "1.1", optional = true }
|
||||
|
||||
# mips
|
||||
rabbitizer = { version = "2.0.0-alpha.4", default-features = false, features = ["all_extensions"], optional = true }
|
||||
rabbitizer = { version = "2.0.0-alpha.9", default-features = false, features = ["all_extensions"], optional = true }
|
||||
|
||||
# x86
|
||||
cpp_demangle = { version = "0.4", default-features = false, features = ["alloc"], optional = true }
|
||||
iced-x86 = { version = "1.21", default-features = false, features = ["decoder", "intel", "gas", "masm", "nasm", "exhaustive_enums", "no_std"], optional = true }
|
||||
msvc-demangler = { version = "0.11", optional = true }
|
||||
|
||||
# arm
|
||||
unarm = { version = "1.9", optional = true }
|
||||
unarm = { version = "2.1", optional = true }
|
||||
arm-attr = { version = "0.2", optional = true }
|
||||
|
||||
# arm64
|
||||
@@ -172,29 +169,35 @@ yaxpeax-arch = { version = "0.3", default-features = false, optional = true }
|
||||
yaxpeax-arm = { version = "0.3", default-features = false, optional = true }
|
||||
|
||||
# build
|
||||
notify = { version = "8.1.0", optional = true }
|
||||
notify-debouncer-full = { version = "0.5.0", optional = true }
|
||||
notify = { version = "8.2.0", optional = true }
|
||||
notify-debouncer-full = { version = "0.6.0", optional = true }
|
||||
shell-escape = { version = "0.1", optional = true }
|
||||
tempfile = { version = "3.20", optional = true }
|
||||
tempfile = { version = "3.23", optional = true }
|
||||
time = { version = "0.3", optional = true }
|
||||
encoding_rs = { version = "0.8.35", optional = true }
|
||||
|
||||
# demangler
|
||||
cpp_demangle = { version = "0.5", optional = true, default-features = false, features = ["alloc"] }
|
||||
cwdemangle = { version = "1.0", optional = true }
|
||||
gnuv2_demangle = { version = "0.4", optional = true }
|
||||
msvc-demangler = { version = "0.11", optional = true }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
winapi = { version = "0.3", optional = true, features = ["winbase"] }
|
||||
|
||||
# For Linux static binaries, use rustls
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "multipart", "rustls-tls"], optional = true }
|
||||
self_update = { version = "0.42", default-features = false, features = ["rustls"], optional = true }
|
||||
reqwest = { version = "0.13", default-features = false, features = ["blocking", "json", "multipart", "rustls"], optional = true }
|
||||
self_update = { version = "1.0.0-rc.3", default-features = false, features = ["reqwest", "rustls", "github"], optional = true }
|
||||
|
||||
# For all other platforms, use native TLS
|
||||
[target.'cfg(not(target_os = "linux"))'.dependencies]
|
||||
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "multipart", "default-tls"], optional = true }
|
||||
self_update = { version = "0.42", optional = true }
|
||||
reqwest = { version = "0.13", default-features = false, features = ["blocking", "json", "multipart", "default-tls"], optional = true }
|
||||
self_update = { version = "1.0.0-rc.3", default-features = false, features = ["reqwest", "native-tls", "github"], optional = true }
|
||||
|
||||
[build-dependencies]
|
||||
heck = { version = "0.5", optional = true }
|
||||
pbjson-build = { version = "0.8", optional = true }
|
||||
pbjson-build = { version = "0.9", optional = true }
|
||||
prettyplease = { version = "0.2", optional = true }
|
||||
proc-macro2 = { version = "1.0", optional = true }
|
||||
prost-build = { version = "0.14", optional = true }
|
||||
|
||||
+151
-12
@@ -25,6 +25,42 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "demangler",
|
||||
"type": "choice",
|
||||
"default": "auto",
|
||||
"name": "Demangler",
|
||||
"description": "Which demangler should be used to demangle each symbol.",
|
||||
"items": [
|
||||
{
|
||||
"value": "auto",
|
||||
"name": "Auto",
|
||||
"description": "Try to automatically guess the mangling format."
|
||||
},
|
||||
{
|
||||
"value": "none",
|
||||
"name": "None",
|
||||
"description": "Disable demangling."
|
||||
},
|
||||
{
|
||||
"value": "codewarrior",
|
||||
"name": "CodeWarrior"
|
||||
},
|
||||
{
|
||||
"value": "itanium",
|
||||
"name": "Itanium"
|
||||
},
|
||||
{
|
||||
"value": "msvc",
|
||||
"name": "MSVC"
|
||||
},
|
||||
{
|
||||
"value": "gnu_legacy",
|
||||
"name": "GNU g++ (Legacy)",
|
||||
"description": "Use the old GNU mangling ABI. Used up to g++ 2.9.x"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "analyzeDataFlow",
|
||||
"type": "boolean",
|
||||
@@ -46,6 +82,27 @@
|
||||
"name": "Space between args",
|
||||
"description": "Adds a space between arguments in the diff output."
|
||||
},
|
||||
{
|
||||
"id": "showSymbolSizes",
|
||||
"type": "choice",
|
||||
"default": "off",
|
||||
"name": "Show symbol sizes",
|
||||
"description": "Shows symbol sizes in the symbol view.",
|
||||
"items": [
|
||||
{
|
||||
"value": "off",
|
||||
"name": "Off"
|
||||
},
|
||||
{
|
||||
"value": "hex",
|
||||
"name": "Hex"
|
||||
},
|
||||
{
|
||||
"value": "decimal",
|
||||
"name": "Decimal"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "combineDataSections",
|
||||
"type": "boolean",
|
||||
@@ -60,6 +117,51 @@
|
||||
"name": "Combine text sections",
|
||||
"description": "Combines all text sections into one."
|
||||
},
|
||||
{
|
||||
"id": "preferredStringEncoding",
|
||||
"type": "choice",
|
||||
"default": "auto",
|
||||
"name": "Preferred string encoding",
|
||||
"description": "Which encoding to use when diffing string literals used in functions.",
|
||||
"items": [
|
||||
{
|
||||
"value": "auto",
|
||||
"name": "Auto"
|
||||
},
|
||||
{
|
||||
"value": "ascii",
|
||||
"name": "ASCII"
|
||||
},
|
||||
{
|
||||
"value": "utf_8",
|
||||
"name": "UTF-8"
|
||||
},
|
||||
{
|
||||
"value": "shift_jis",
|
||||
"name": "Shift JIS"
|
||||
},
|
||||
{
|
||||
"value": "windows_1252",
|
||||
"name": "Windows-1252"
|
||||
},
|
||||
{
|
||||
"value": "euc_jp",
|
||||
"name": "EUC-JP"
|
||||
},
|
||||
{
|
||||
"value": "big5",
|
||||
"name": "Big5"
|
||||
},
|
||||
{
|
||||
"value": "utf_16be",
|
||||
"name": "UTF-16BE"
|
||||
},
|
||||
{
|
||||
"value": "utf_16le",
|
||||
"name": "UTF-16LE"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "arm.archVersion",
|
||||
"type": "choice",
|
||||
@@ -71,20 +173,43 @@
|
||||
"value": "auto",
|
||||
"name": "Auto"
|
||||
},
|
||||
{
|
||||
"value": "v4",
|
||||
"name": "ARMv4"
|
||||
},
|
||||
{
|
||||
"value": "v4t",
|
||||
"name": "ARMv4T (GBA)"
|
||||
},
|
||||
{
|
||||
"value": "v5t",
|
||||
"name": "ARMv5T"
|
||||
},
|
||||
{
|
||||
"value": "v5te",
|
||||
"name": "ARMv5TE (DS)"
|
||||
},
|
||||
{
|
||||
"value": "v5tej",
|
||||
"name": "ARMv5TEJ"
|
||||
},
|
||||
{
|
||||
"value": "v6",
|
||||
"name": "ARMv6"
|
||||
},
|
||||
{
|
||||
"value": "v6k",
|
||||
"name": "ARMv6K (3DS)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "arm.vfpV2",
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"name": "VFPv2 instructions",
|
||||
"description": "Adds floating-point instructions from VFPv2."
|
||||
},
|
||||
{
|
||||
"id": "arm.unifiedSyntax",
|
||||
"type": "boolean",
|
||||
@@ -158,6 +283,10 @@
|
||||
"value": "o32",
|
||||
"name": "O32"
|
||||
},
|
||||
{
|
||||
"value": "o64",
|
||||
"name": "O64"
|
||||
},
|
||||
{
|
||||
"value": "n32",
|
||||
"name": "N32"
|
||||
@@ -165,6 +294,14 @@
|
||||
{
|
||||
"value": "n64",
|
||||
"name": "N64"
|
||||
},
|
||||
{
|
||||
"value": "eabi32",
|
||||
"name": "eabi32"
|
||||
},
|
||||
{
|
||||
"value": "eabi64",
|
||||
"name": "eabi64"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -208,6 +345,13 @@
|
||||
"name": "Register '$' prefix",
|
||||
"description": "Display MIPS register names with a '$' prefix."
|
||||
},
|
||||
{
|
||||
"id": "mips.prodgSnAsInvertedRegs",
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"name": "ProDG SN AS VU register order",
|
||||
"description": "Changes the register order for specific R5900EE instructions to match the syntax used by the SN AS from ProDG (-snas flag)."
|
||||
},
|
||||
{
|
||||
"id": "ppc.calculatePoolRelocations",
|
||||
"type": "boolean",
|
||||
@@ -246,7 +390,10 @@
|
||||
"id": "general",
|
||||
"name": "General",
|
||||
"properties": [
|
||||
"preferredStringEncoding",
|
||||
"functionRelocDiffs",
|
||||
"demangler",
|
||||
"showSymbolSizes",
|
||||
"spaceBetweenArgs",
|
||||
"combineDataSections",
|
||||
"combineTextSections"
|
||||
@@ -257,6 +404,7 @@
|
||||
"name": "ARM",
|
||||
"properties": [
|
||||
"arm.archVersion",
|
||||
"arm.vfpV2",
|
||||
"arm.unifiedSyntax",
|
||||
"arm.avRegisters",
|
||||
"arm.r9Usage",
|
||||
@@ -268,26 +416,17 @@
|
||||
{
|
||||
"id": "mips",
|
||||
"name": "MIPS",
|
||||
"properties": [
|
||||
"mips.abi",
|
||||
"mips.instrCategory",
|
||||
"mips.registerPrefix"
|
||||
]
|
||||
"properties": ["mips.abi", "mips.instrCategory", "mips.registerPrefix", "mips.prodgSnAsInvertedRegs"]
|
||||
},
|
||||
{
|
||||
"id": "ppc",
|
||||
"name": "PowerPC",
|
||||
"properties": [
|
||||
"ppc.calculatePoolRelocations",
|
||||
"analyzeDataFlow"
|
||||
]
|
||||
"properties": ["ppc.calculatePoolRelocations", "analyzeDataFlow"]
|
||||
},
|
||||
{
|
||||
"id": "x86",
|
||||
"name": "x86",
|
||||
"properties": [
|
||||
"x86.formatter"
|
||||
]
|
||||
"properties": ["x86.formatter"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package objdiff.diff;
|
||||
|
||||
// Top-level diff result containing left and right object diffs
|
||||
message DiffResult {
|
||||
optional DiffObject left = 1;
|
||||
optional DiffObject right = 2;
|
||||
}
|
||||
|
||||
// Diff information for a single object file
|
||||
message DiffObject {
|
||||
repeated DiffSection sections = 1;
|
||||
repeated DiffSymbol symbols = 2;
|
||||
}
|
||||
|
||||
// Diff information for a section
|
||||
message DiffSection {
|
||||
string name = 1;
|
||||
DiffSectionKind kind = 2;
|
||||
uint64 size = 3;
|
||||
uint64 address = 4;
|
||||
optional float match_percent = 5;
|
||||
repeated DiffDataSegment data_diff = 6;
|
||||
repeated DiffDataRelocation reloc_diff = 7;
|
||||
}
|
||||
|
||||
enum DiffSectionKind {
|
||||
SECTION_UNKNOWN = 0;
|
||||
SECTION_CODE = 1;
|
||||
SECTION_DATA = 2;
|
||||
SECTION_BSS = 3;
|
||||
SECTION_COMMON = 4;
|
||||
}
|
||||
|
||||
// Symbol with diff information
|
||||
message DiffSymbol {
|
||||
string name = 1;
|
||||
optional string demangled_name = 2;
|
||||
uint64 address = 3;
|
||||
uint64 size = 4;
|
||||
DiffSymbolFlags flags = 5;
|
||||
DiffSymbolKind kind = 10;
|
||||
// The symbol index in the _other_ object that this symbol was diffed against
|
||||
optional uint32 target_symbol = 6;
|
||||
optional float match_percent = 7;
|
||||
// Instruction diff rows (for code symbols)
|
||||
repeated DiffInstructionRow instructions = 8;
|
||||
// Data diff (for data symbols)
|
||||
repeated DiffDataSegment data_diff = 9;
|
||||
}
|
||||
|
||||
enum DiffSymbolKind {
|
||||
SYMBOL_UNKNOWN = 0;
|
||||
SYMBOL_FUNCTION = 1;
|
||||
SYMBOL_OBJECT = 2;
|
||||
SYMBOL_SECTION = 3;
|
||||
}
|
||||
|
||||
// Symbol flags
|
||||
message DiffSymbolFlags {
|
||||
bool global = 1;
|
||||
bool local = 2;
|
||||
bool weak = 3;
|
||||
bool common = 4;
|
||||
bool hidden = 5;
|
||||
bool ignored = 6;
|
||||
bool size_inferred = 7;
|
||||
}
|
||||
|
||||
// A single instruction diff row
|
||||
message DiffInstructionRow {
|
||||
DiffKind diff_kind = 1;
|
||||
optional DiffInstruction instruction = 2;
|
||||
repeated DiffInstructionArgDiff arg_diff = 3;
|
||||
}
|
||||
|
||||
// Parsed instruction information
|
||||
message DiffInstruction {
|
||||
uint64 address = 1;
|
||||
uint32 size = 2;
|
||||
// Formatted instruction string
|
||||
string formatted = 3;
|
||||
// Formatted instruction parts
|
||||
repeated DiffInstructionPart parts = 4;
|
||||
// Relocation information (if present)
|
||||
optional DiffRelocation relocation = 5;
|
||||
// Branch destination address
|
||||
optional uint64 branch_dest = 6;
|
||||
// Source line number (if present)
|
||||
optional uint32 line_number = 7;
|
||||
}
|
||||
|
||||
// An instruction part
|
||||
message DiffInstructionPart {
|
||||
oneof part {
|
||||
// Basic text (whitespace, punctuation, etc.)
|
||||
string basic = 1;
|
||||
// Opcode/mnemonic
|
||||
DiffOpcode opcode = 2;
|
||||
// Argument
|
||||
DiffInstructionArg arg = 3;
|
||||
// Separator between arguments (comma)
|
||||
bool separator = 4;
|
||||
}
|
||||
}
|
||||
|
||||
message DiffOpcode {
|
||||
// Mnemonic string
|
||||
string mnemonic = 1;
|
||||
// Base opcode ID
|
||||
uint32 opcode = 2;
|
||||
}
|
||||
|
||||
// An instruction argument
|
||||
message DiffInstructionArg {
|
||||
oneof arg {
|
||||
// Signed immediate value
|
||||
sint64 signed = 1;
|
||||
// Unsigned immediate value
|
||||
uint64 unsigned = 2;
|
||||
// Opaque string value (register names, etc.)
|
||||
string opaque = 3;
|
||||
// Relocation
|
||||
bool reloc = 4;
|
||||
// Branch destination address
|
||||
uint64 branch_dest = 5;
|
||||
}
|
||||
}
|
||||
|
||||
// Relocation information
|
||||
message DiffRelocation {
|
||||
uint32 type = 1;
|
||||
string type_name = 2;
|
||||
uint32 target_symbol = 3;
|
||||
int64 addend = 4;
|
||||
}
|
||||
|
||||
// Argument diff information
|
||||
message DiffInstructionArgDiff {
|
||||
// If set, this argument differs from the other side.
|
||||
// The value is a rotating index for coloring.
|
||||
optional uint32 diff_index = 1;
|
||||
}
|
||||
|
||||
// Diff kind for instructions and data
|
||||
enum DiffKind {
|
||||
DIFF_NONE = 0;
|
||||
DIFF_REPLACE = 1;
|
||||
DIFF_DELETE = 2;
|
||||
DIFF_INSERT = 3;
|
||||
DIFF_OP_MISMATCH = 4;
|
||||
DIFF_ARG_MISMATCH = 5;
|
||||
}
|
||||
|
||||
// Data diff segment
|
||||
message DiffDataSegment {
|
||||
DiffKind kind = 1;
|
||||
bytes data = 2;
|
||||
// Size may be larger than data length for BSS
|
||||
uint64 size = 3;
|
||||
}
|
||||
|
||||
// Data relocation diff
|
||||
message DiffDataRelocation {
|
||||
DiffRelocation relocation = 1;
|
||||
DiffKind kind = 2;
|
||||
// Address range this relocation covers
|
||||
uint64 start = 3;
|
||||
uint64 end = 4;
|
||||
}
|
||||
Binary file not shown.
+313
-370
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,4 @@
|
||||
use alloc::{
|
||||
format,
|
||||
string::{String, ToString},
|
||||
vec::Vec,
|
||||
};
|
||||
use alloc::{format, string::ToString, vec::Vec};
|
||||
use core::cmp::Ordering;
|
||||
|
||||
use anyhow::Result;
|
||||
@@ -14,7 +10,7 @@ use yaxpeax_arm::armv8::a64::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
arch::Arch,
|
||||
arch::{Arch, OPCODE_INVALID},
|
||||
diff::{DiffObjConfig, display::InstructionPart},
|
||||
obj::{
|
||||
InstructionRef, Relocation, RelocationFlags, ResolvedInstructionRef, ResolvedRelocation,
|
||||
@@ -60,7 +56,7 @@ impl Arch for ArchArm64 {
|
||||
ops.push(InstructionRef {
|
||||
address,
|
||||
size: 4,
|
||||
opcode: u16::MAX,
|
||||
opcode: OPCODE_INVALID,
|
||||
branch_dest: None,
|
||||
});
|
||||
continue;
|
||||
@@ -87,7 +83,7 @@ impl Arch for ArchArm64 {
|
||||
let decoder = InstDecoder::default();
|
||||
let mut ins = Instruction::default();
|
||||
if decoder.decode_into(&mut ins, &mut reader).is_err() {
|
||||
cb(InstructionPart::opcode("<invalid>", u16::MAX))?;
|
||||
cb(InstructionPart::opcode("<invalid>", OPCODE_INVALID))?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -108,12 +104,6 @@ impl Arch for ArchArm64 {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn demangle(&self, name: &str) -> Option<String> {
|
||||
cpp_demangle::Symbol::new(name)
|
||||
.ok()
|
||||
.and_then(|s| s.demangle(&cpp_demangle::DemangleOptions::default()).ok())
|
||||
}
|
||||
|
||||
fn reloc_name(&self, flags: RelocationFlags) -> Option<&'static str> {
|
||||
match flags {
|
||||
RelocationFlags::Elf(r_type) => match r_type {
|
||||
@@ -2295,7 +2285,7 @@ where Cb: FnMut(InstructionPart<'static>) {
|
||||
// Opcode is #[repr(u16)], but the tuple variants negate that, so we have to do this instead.
|
||||
const fn opcode_to_u16(opcode: Opcode) -> u16 {
|
||||
match opcode {
|
||||
Opcode::Invalid => u16::MAX,
|
||||
Opcode::Invalid => OPCODE_INVALID,
|
||||
Opcode::UDF => 0,
|
||||
Opcode::MOVN => 1,
|
||||
Opcode::MOVK => 2,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use alloc::{
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
string::{String, ToString},
|
||||
string::ToString,
|
||||
vec::Vec,
|
||||
};
|
||||
|
||||
@@ -46,8 +46,11 @@ impl ArchMips {
|
||||
object::FileFlags::None => {}
|
||||
object::FileFlags::Elf { e_flags, .. } => {
|
||||
abi = match e_flags & EF_MIPS_ABI {
|
||||
elf::EF_MIPS_ABI_O32 | elf::EF_MIPS_ABI_O64 => Abi::O32,
|
||||
elf::EF_MIPS_ABI_EABI32 | elf::EF_MIPS_ABI_EABI64 => Abi::N32,
|
||||
elf::EF_MIPS_ABI_O32 => Abi::O32,
|
||||
elf::EF_MIPS_ABI_O64 if e_flags & elf::EF_MIPS_ABI2 != 0 => Abi::N64,
|
||||
elf::EF_MIPS_ABI_O64 => Abi::O64,
|
||||
elf::EF_MIPS_ABI_EABI32 => Abi::EABI32,
|
||||
elf::EF_MIPS_ABI_EABI64 => Abi::EABI64,
|
||||
_ => {
|
||||
if e_flags & elf::EF_MIPS_ABI2 != 0 {
|
||||
Abi::N32
|
||||
@@ -74,7 +77,7 @@ impl ArchMips {
|
||||
.and_then(|section| section.data().ok())
|
||||
.and_then(|data| data.get(0x14..0x18))
|
||||
.and_then(|s| s.try_into().ok())
|
||||
.map(|bytes| endianness.read_i32_bytes(bytes))
|
||||
.map(|bytes| endianness.read_i32(bytes))
|
||||
.unwrap_or(0);
|
||||
|
||||
// Parse all relocations to pair R_MIPS_HI16 and R_MIPS_LO16. Since the instructions only
|
||||
@@ -95,13 +98,13 @@ impl ArchMips {
|
||||
match reloc.flags() {
|
||||
object::RelocationFlags::Elf { r_type: elf::R_MIPS_HI16 } => {
|
||||
let code = data[addr as usize..addr as usize + 4].try_into()?;
|
||||
let addend = ((endianness.read_u32_bytes(code) & 0x0000FFFF) << 16) as i32;
|
||||
let addend = ((endianness.read_u32(code) & 0x0000FFFF) << 16) as i32;
|
||||
last_hi = Some(addr);
|
||||
last_hi_addend = addend;
|
||||
}
|
||||
object::RelocationFlags::Elf { r_type: elf::R_MIPS_LO16 } => {
|
||||
let code = data[addr as usize..addr as usize + 4].try_into()?;
|
||||
let addend = (endianness.read_u32_bytes(code) & 0x0000FFFF) as i16 as i32;
|
||||
let addend = (endianness.read_u32(code) & 0x0000FFFF) as i16 as i32;
|
||||
let full_addend = (last_hi_addend + addend) as i64;
|
||||
if let Some(hi_addr) = last_hi.take() {
|
||||
addends.insert(hi_addr, full_addend);
|
||||
@@ -170,8 +173,11 @@ impl ArchMips {
|
||||
.with_abi(match diff_config.mips_abi {
|
||||
MipsAbi::Auto => self.abi,
|
||||
MipsAbi::O32 => Abi::O32,
|
||||
MipsAbi::O64 => Abi::O64,
|
||||
MipsAbi::N32 => Abi::N32,
|
||||
MipsAbi::N64 => Abi::N64,
|
||||
MipsAbi::Eabi32 => Abi::EABI32,
|
||||
MipsAbi::Eabi64 => Abi::EABI64,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -182,6 +188,7 @@ impl ArchMips {
|
||||
rabbitizer::InstructionDisplayFlags::default()
|
||||
.with_unknown_instr_comment(false)
|
||||
.with_use_dollar(diff_config.mips_register_prefix)
|
||||
.with_r5900ee_prodg_sn_as_inverted_regs(diff_config.mips_prodg_sn_as_inverted_regs)
|
||||
}
|
||||
|
||||
fn parse_ins_ref(
|
||||
@@ -191,7 +198,7 @@ impl ArchMips {
|
||||
diff_config: &DiffObjConfig,
|
||||
) -> Result<rabbitizer::Instruction> {
|
||||
Ok(rabbitizer::Instruction::new(
|
||||
self.endianness.read_u32_bytes(code.try_into()?),
|
||||
self.endianness.read_u32(code.try_into()?),
|
||||
Vram::new(ins_ref.address as u32),
|
||||
self.instruction_flags(diff_config),
|
||||
))
|
||||
@@ -210,8 +217,8 @@ impl Arch for ArchMips {
|
||||
let instruction_flags = self.instruction_flags(diff_config);
|
||||
let mut ops = Vec::<InstructionRef>::with_capacity(code.len() / 4);
|
||||
let mut cur_addr = address as u32;
|
||||
for chunk in code.chunks_exact(4) {
|
||||
let code = self.endianness.read_u32_bytes(chunk.try_into()?);
|
||||
for chunk in code.as_chunks::<4>().0 {
|
||||
let code = self.endianness.read_u32(*chunk);
|
||||
let instruction =
|
||||
rabbitizer::Instruction::new(code, Vram::new(cur_addr), instruction_flags);
|
||||
let opcode = instruction.opcode() as u16;
|
||||
@@ -231,7 +238,8 @@ impl Arch for ArchMips {
|
||||
let instruction = self.parse_ins_ref(resolved.ins_ref, resolved.code, diff_config)?;
|
||||
let display_flags = self.instruction_display_flags(diff_config);
|
||||
let opcode = instruction.opcode();
|
||||
cb(InstructionPart::opcode(opcode.name(), opcode as u16))?;
|
||||
let mnemonic = instruction.mnemonic_display(&display_flags).to_string();
|
||||
cb(InstructionPart::opcode(mnemonic, opcode as u16))?;
|
||||
push_args(&instruction, resolved.relocation, &display_flags, cb)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -263,7 +271,7 @@ impl Arch for ArchMips {
|
||||
let data = section.data()?;
|
||||
let code = self
|
||||
.endianness
|
||||
.read_u32_bytes(data[address as usize..address as usize + 4].try_into()?);
|
||||
.read_u32(data[address as usize..address as usize + 4].try_into()?);
|
||||
let addend = match r_type {
|
||||
elf::R_MIPS_32 => code as i64,
|
||||
elf::R_MIPS_26 => ((code & 0x03FFFFFF) << 2) as i64,
|
||||
@@ -287,6 +295,7 @@ impl Arch for ArchMips {
|
||||
}
|
||||
elf::R_MIPS_PC16 => 0, // PC-relative relocation
|
||||
R_MIPS15_S3 => ((code & 0x001FFFC0) >> 3) as i64,
|
||||
elf::R_MIPS_GPREL32 => (code as i32 as i64) + self.ri_gp_value as i64,
|
||||
flags => bail!("Unsupported MIPS implicit relocation {flags:?}"),
|
||||
};
|
||||
Ok(Some(RelocationOverride { target: RelocationOverrideTarget::Keep, addend }))
|
||||
@@ -298,13 +307,6 @@ impl Arch for ArchMips {
|
||||
}
|
||||
}
|
||||
|
||||
fn demangle(&self, name: &str) -> Option<String> {
|
||||
cpp_demangle::Symbol::new(name)
|
||||
.ok()
|
||||
.and_then(|s| s.demangle(&cpp_demangle::DemangleOptions::default()).ok())
|
||||
.or_else(|| cwdemangle::demangle(name, &cwdemangle::DemangleOptions::default()))
|
||||
}
|
||||
|
||||
fn reloc_name(&self, flags: RelocationFlags) -> Option<&'static str> {
|
||||
match flags {
|
||||
RelocationFlags::Elf(r_type) => match r_type {
|
||||
@@ -319,6 +321,7 @@ impl Arch for ArchMips {
|
||||
elf::R_MIPS_GOT16 => Some("R_MIPS_GOT16"),
|
||||
elf::R_MIPS_PC16 => Some("R_MIPS_PC16"),
|
||||
elf::R_MIPS_CALL16 => Some("R_MIPS_CALL16"),
|
||||
elf::R_MIPS_GPREL32 => Some("R_MIPS_GPREL32"),
|
||||
R_MIPS15_S3 => Some("R_MIPS15_S3"),
|
||||
_ => None,
|
||||
},
|
||||
@@ -356,6 +359,7 @@ impl Arch for ArchMips {
|
||||
while new_address >= symbol.address + 4
|
||||
&& let Some(data) = section.data_range(new_address - 4, 4)
|
||||
&& data == [0u8; 4]
|
||||
&& section.relocation_at(next_address - 4, 4).is_none()
|
||||
{
|
||||
new_address -= 4;
|
||||
}
|
||||
@@ -364,7 +368,7 @@ impl Arch for ArchMips {
|
||||
&& new_address >= symbol.address + 4
|
||||
&& let Some(data) = section.data_range(new_address - 4, 4)
|
||||
&& let instruction = rabbitizer::Instruction::new(
|
||||
self.endianness.read_u32_bytes(data.try_into().unwrap()),
|
||||
self.endianness.read_u32(data.try_into().unwrap()),
|
||||
Vram::new((new_address - 4) as u32),
|
||||
self.default_instruction_flags(),
|
||||
)
|
||||
|
||||
+143
-43
@@ -7,17 +7,15 @@ use alloc::{
|
||||
};
|
||||
use core::{
|
||||
any::Any,
|
||||
ffi::CStr,
|
||||
fmt::{self, Debug},
|
||||
};
|
||||
|
||||
use anyhow::{Result, bail};
|
||||
use encoding_rs::SHIFT_JIS;
|
||||
use object::Endian as _;
|
||||
|
||||
use crate::{
|
||||
diff::{
|
||||
DiffObjConfig, DiffSide,
|
||||
ConfigEnum, DiffObjConfig, DiffSide, PreferredStringEncoding,
|
||||
display::{ContextItem, HoverItem, InstructionPart},
|
||||
},
|
||||
obj::{
|
||||
@@ -41,6 +39,46 @@ pub mod superh;
|
||||
#[cfg(feature = "x86")]
|
||||
pub mod x86;
|
||||
|
||||
pub const OPCODE_INVALID: u16 = u16::MAX;
|
||||
pub const OPCODE_DATA: u16 = u16::MAX - 1;
|
||||
|
||||
const SUPPORTED_ENCODINGS_WITH_NULL_TERM: [(&encoding_rs::Encoding, &str); 5] = [
|
||||
(encoding_rs::UTF_8, "UTF-8"),
|
||||
(encoding_rs::SHIFT_JIS, "Shift JIS"),
|
||||
(encoding_rs::WINDOWS_1252, "Windows-1252"),
|
||||
(encoding_rs::EUC_JP, "EUC-JP"),
|
||||
(encoding_rs::BIG5, "Big5"),
|
||||
];
|
||||
const SUPPORTED_ENCODINGS_NO_NULL_TERM: [(&encoding_rs::Encoding, &str); 2] =
|
||||
[(encoding_rs::UTF_16BE, "UTF-16BE"), (encoding_rs::UTF_16LE, "UTF-16LE")];
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq)]
|
||||
pub struct LiteralInfo {
|
||||
pub literal: String,
|
||||
pub label_override: Option<String>,
|
||||
pub copy_string: Option<String>,
|
||||
pub hidden: bool, // Only used when the user hasn't set a preferred string encoding
|
||||
pub is_string: bool,
|
||||
}
|
||||
|
||||
impl LiteralInfo {
|
||||
pub fn hidden(&self, diff_config: Option<&DiffObjConfig>) -> bool {
|
||||
let Some(diff_config) = diff_config else {
|
||||
return self.hidden;
|
||||
};
|
||||
if !self.is_string {
|
||||
return self.hidden;
|
||||
}
|
||||
if diff_config.preferred_string_encoding == PreferredStringEncoding::Auto {
|
||||
return self.hidden;
|
||||
}
|
||||
let Some(ref label) = self.label_override else {
|
||||
return self.hidden;
|
||||
};
|
||||
*label != diff_config.preferred_string_encoding.name()
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the type of data associated with an instruction
|
||||
#[derive(PartialEq)]
|
||||
pub enum DataType {
|
||||
@@ -72,18 +110,14 @@ impl fmt::Display for DataType {
|
||||
impl DataType {
|
||||
pub fn display_labels(&self, endian: object::Endianness, bytes: &[u8]) -> Vec<String> {
|
||||
let mut strs = Vec::new();
|
||||
for (literal, label_override) in self.display_literals(endian, bytes) {
|
||||
let label = label_override.unwrap_or_else(|| self.to_string());
|
||||
strs.push(format!("{label}: {literal}"))
|
||||
for lit_info in self.display_literals(endian, bytes) {
|
||||
let label = lit_info.label_override.unwrap_or_else(|| self.to_string());
|
||||
strs.push(format!("{}: {:?}", label, lit_info.literal))
|
||||
}
|
||||
strs
|
||||
}
|
||||
|
||||
pub fn display_literals(
|
||||
&self,
|
||||
endian: object::Endianness,
|
||||
bytes: &[u8],
|
||||
) -> Vec<(String, Option<String>)> {
|
||||
pub fn display_literals(&self, endian: object::Endianness, bytes: &[u8]) -> Vec<LiteralInfo> {
|
||||
let mut strs = Vec::new();
|
||||
if self.required_len().is_some_and(|l| bytes.len() < l) {
|
||||
log::warn!(
|
||||
@@ -107,73 +141,122 @@ impl DataType {
|
||||
match self {
|
||||
DataType::Int8 => {
|
||||
let i = i8::from_ne_bytes(bytes.try_into().unwrap());
|
||||
strs.push((format!("{i:#x}"), None));
|
||||
strs.push(LiteralInfo { literal: format!("{i:#x}"), ..Default::default() });
|
||||
|
||||
if i < 0 {
|
||||
strs.push((format!("{:#x}", ReallySigned(i)), None));
|
||||
strs.push(LiteralInfo {
|
||||
literal: format!("{:#x}", ReallySigned(i)),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
DataType::Int16 => {
|
||||
let i = endian.read_i16_bytes(bytes.try_into().unwrap());
|
||||
strs.push((format!("{i:#x}"), None));
|
||||
let i = endian.read_i16(bytes.try_into().unwrap());
|
||||
strs.push(LiteralInfo { literal: format!("{i:#x}"), ..Default::default() });
|
||||
|
||||
if i < 0 {
|
||||
strs.push((format!("{:#x}", ReallySigned(i)), None));
|
||||
strs.push(LiteralInfo {
|
||||
literal: format!("{:#x}", ReallySigned(i)),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
DataType::Int32 => {
|
||||
let i = endian.read_i32_bytes(bytes.try_into().unwrap());
|
||||
strs.push((format!("{i:#x}"), None));
|
||||
let i = endian.read_i32(bytes.try_into().unwrap());
|
||||
strs.push(LiteralInfo { literal: format!("{i:#x}"), ..Default::default() });
|
||||
|
||||
if i < 0 {
|
||||
strs.push((format!("{:#x}", ReallySigned(i)), None));
|
||||
strs.push(LiteralInfo {
|
||||
literal: format!("{:#x}", ReallySigned(i)),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
DataType::Int64 => {
|
||||
let i = endian.read_i64_bytes(bytes.try_into().unwrap());
|
||||
strs.push((format!("{i:#x}"), None));
|
||||
let i = endian.read_i64(bytes.try_into().unwrap());
|
||||
strs.push(LiteralInfo { literal: format!("{i:#x}"), ..Default::default() });
|
||||
|
||||
if i < 0 {
|
||||
strs.push((format!("{:#x}", ReallySigned(i)), None));
|
||||
strs.push(LiteralInfo {
|
||||
literal: format!("{:#x}", ReallySigned(i)),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
DataType::Float => {
|
||||
let bytes: [u8; 4] = bytes.try_into().unwrap();
|
||||
strs.push((
|
||||
format!("{:?}f", match endian {
|
||||
strs.push(LiteralInfo {
|
||||
literal: format!("{:?}f", match endian {
|
||||
object::Endianness::Little => f32::from_le_bytes(bytes),
|
||||
object::Endianness::Big => f32::from_be_bytes(bytes),
|
||||
}),
|
||||
None,
|
||||
));
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
DataType::Double => {
|
||||
let bytes: [u8; 8] = bytes.try_into().unwrap();
|
||||
strs.push((
|
||||
format!("{:?}", match endian {
|
||||
strs.push(LiteralInfo {
|
||||
literal: format!("{:?}", match endian {
|
||||
object::Endianness::Little => f64::from_le_bytes(bytes),
|
||||
object::Endianness::Big => f64::from_be_bytes(bytes),
|
||||
}),
|
||||
None,
|
||||
));
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
DataType::Bytes => {
|
||||
strs.push((format!("{bytes:#?}"), None));
|
||||
strs.push(LiteralInfo { literal: format!("{bytes:#?}"), ..Default::default() });
|
||||
}
|
||||
DataType::String => {
|
||||
if let Ok(cstr) = CStr::from_bytes_until_nul(bytes) {
|
||||
strs.push((format!("{cstr:?}"), None));
|
||||
}
|
||||
if let Some(nul_idx) = bytes.iter().position(|&c| c == b'\0') {
|
||||
let (cow, _, had_errors) = SHIFT_JIS.decode(&bytes[..nul_idx]);
|
||||
if !had_errors {
|
||||
let str = format!("{cow:?}");
|
||||
// Only add the Shift JIS string if it's different from the ASCII string.
|
||||
if !strs.iter().any(|x| x.0 == str) {
|
||||
strs.push((str, Some("Shift JIS".into())));
|
||||
let str_bytes = &bytes[..nul_idx];
|
||||
// Special case to display (ASCII) as the label for ASCII-only strings.
|
||||
let (cow, _, had_errors) = encoding_rs::UTF_8.decode(str_bytes);
|
||||
if !had_errors && cow.is_ascii() {
|
||||
let string = format!("{cow}");
|
||||
let copy_string = escape_special_ascii_characters(&string);
|
||||
strs.push(LiteralInfo {
|
||||
literal: string,
|
||||
label_override: Some("ASCII".into()),
|
||||
copy_string: Some(copy_string),
|
||||
hidden: false,
|
||||
is_string: true,
|
||||
});
|
||||
}
|
||||
for (encoding, encoding_name) in SUPPORTED_ENCODINGS_WITH_NULL_TERM {
|
||||
let (cow, _, had_errors) = encoding.decode(str_bytes);
|
||||
if !had_errors {
|
||||
let string = format!("{cow}");
|
||||
let copy_string = escape_special_ascii_characters(&string);
|
||||
// Avoid showing ASCII-only strings more than once if the encoding is ASCII-compatible.
|
||||
let hidden = encoding.is_ascii_compatible() && cow.is_ascii();
|
||||
strs.push(LiteralInfo {
|
||||
literal: string,
|
||||
label_override: Some(encoding_name.into()),
|
||||
copy_string: Some(copy_string),
|
||||
hidden,
|
||||
is_string: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (encoding, encoding_name) in SUPPORTED_ENCODINGS_NO_NULL_TERM {
|
||||
let (cow, _, had_errors) = encoding.decode(bytes);
|
||||
if had_errors {
|
||||
continue;
|
||||
}
|
||||
let trimmed = cow.trim_end_matches('\0');
|
||||
if !trimmed.is_empty() {
|
||||
let copy_string = escape_special_ascii_characters(trimmed);
|
||||
strs.push(LiteralInfo {
|
||||
literal: trimmed.to_string(),
|
||||
label_override: Some(encoding_name.into()),
|
||||
copy_string: Some(copy_string),
|
||||
hidden: false,
|
||||
is_string: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,7 +391,8 @@ impl dyn Arch {
|
||||
|
||||
pub trait Arch: Any + Debug + Send + Sync {
|
||||
/// Finishes arch-specific initialization that must be done after sections have been combined.
|
||||
fn post_init(&mut self, _sections: &[Section], _symbols: &[Symbol]) {}
|
||||
fn post_init(&mut self, _sections: &[Section], _symbols: &[Symbol], _symbol_indices: &[usize]) {
|
||||
}
|
||||
|
||||
/// Generate a list of instructions references (offset, size, opcode) from the given code.
|
||||
///
|
||||
@@ -368,8 +452,6 @@ pub trait Arch: Any + Debug + Send + Sync {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn demangle(&self, _name: &str) -> Option<String> { None }
|
||||
|
||||
fn reloc_name(&self, _flags: RelocationFlags) -> Option<&'static str> { None }
|
||||
|
||||
fn data_reloc_size(&self, flags: RelocationFlags) -> usize;
|
||||
@@ -488,3 +570,21 @@ pub struct RelocationOverride {
|
||||
pub target: RelocationOverrideTarget,
|
||||
pub addend: i64,
|
||||
}
|
||||
|
||||
/// Escape ASCII characters such as \n or \t, but not Unicode characters such as \u{3000}.
|
||||
/// Suitable for copying to clipboard.
|
||||
fn escape_special_ascii_characters(value: &str) -> String {
|
||||
let mut escaped = String::new();
|
||||
escaped.push('"');
|
||||
for c in value.chars() {
|
||||
if c.is_ascii() {
|
||||
for e in c.escape_default() {
|
||||
escaped.push(e);
|
||||
}
|
||||
} else {
|
||||
escaped.push(c);
|
||||
}
|
||||
}
|
||||
escaped.push('"');
|
||||
escaped
|
||||
}
|
||||
|
||||
@@ -513,6 +513,8 @@ pub fn ppc_data_flow_analysis(
|
||||
)
|
||||
}
|
||||
|
||||
// Note: This function only supports MWCC ASCII strings.
|
||||
// Other encodings and other compilers are currently not supported.
|
||||
fn get_string_data(obj: &Object, symbol_index: usize, offset: Simm) -> Option<&str> {
|
||||
if let Some(sym) = obj.symbols.get(symbol_index)
|
||||
&& sym.name.starts_with("@stringBase")
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user