Compare commits

..

18 Commits

Author SHA1 Message Date
d2b7a9ef25 Fix missing common BSS symbols
Resolves #128
2024-10-28 17:54:49 -06:00
2cf9cf24d6 Version v2.3.3 2024-10-20 20:01:35 -07:00
Anghelo Carvajal
5ef3416457 Improve dependency gating on objdiff-core (#126)
* Reduce dependencies for no features

* Add missing deps to every feature

* Add missing `dep:`s

* Gate even more deps behind features

Removes dependency on tsify-next / wasm-bindgen unless
compiling with the wasm feature by using `#[cfg_attr]`

* Fix wasm

---------

Co-authored-by: Luke Street <luke@street.dev>
2024-10-20 19:04:29 -07:00
Aetias
6ff8d002f7 Fix panic when parsing DWARF 2 line info for empty section (#125)
* Fix panic when parsing DWARF 2 line info for empty section

* Fix panic when parsing DWARF 2 line info for empty section
May as well remove both unwraps :p
2024-10-19 09:39:18 -06:00
9ca157d717 Lighten default blue diff color
The old default was very dark and blended in with
the dark theme's background.
2024-10-18 17:51:29 -06:00
Steven Casper
67b63311fc Fix data tooltip panic (#123)
* Fix data tooltip panic

Prevents panicing when attempting to display the data tooltip for a symbol that is too large by just using as many bytes as needed from the begging of the symbol.

* Don't attempt to interpret wrongly sized data

* Reference data display improvment issue

* Log failure to display a symbol's value
2024-10-14 22:03:30 -06:00
72ea1c8911 ci: Use rust-lld on Windows 2024-10-12 18:57:49 -06:00
d4a540857d ci: Add Rust workspace cache 2024-10-12 18:41:42 -06:00
676488433f Fix resolving symbols for section-relative relocations
Also fixes MIPS `j` handling when jumping within the function.

Reworks `ObjReloc` struct to be a little more sensible.
2024-10-11 18:09:18 -06:00
83de98b5ee Version v2.3.1 2024-10-10 22:58:33 -06:00
c1ba4e91d1 ci: Setup python venv for cargo-zigbuild 2024-10-10 22:39:31 -06:00
575900024d Avoid resetting diff state on unit config reload 2024-10-10 22:31:04 -06:00
cbe299e859 Fix logic issue with 0-sized symbols
Fixes #119
2024-10-10 22:20:48 -06:00
741d93e211 Add symbol mapping feature (#118)
This allows users to "map" (or "link") symbols with different names so that they can be compared without having to update either the target or base objects. Symbol mappings are persisted in objdiff.json, so generators will need to ensure that they're preserved when updating. (Example: d1334bb79e)

Resolves #117
2024-10-09 21:44:18 -06:00
603dbd6882 Round match percent down before display
Ensures that 100% isn't displayed until it's a
perfect match.
2024-10-07 20:17:56 -06:00
6fb0a63de2 Click on empty space in row to clear highlight
Resolves #116
2024-10-07 19:53:16 -06:00
ab2e84a2c6 Deprioritize generated GCC symbols in find_section_symbol
Resolves #115
2024-10-07 19:49:52 -06:00
9596051cb4 Allow collapsing sidebar in symbols view 2024-10-07 19:46:16 -06:00
36 changed files with 2656 additions and 1145 deletions

5
.cargo/config.toml Normal file
View File

@@ -0,0 +1,5 @@
[target.x86_64-pc-windows-msvc]
linker = "rust-lld"
[target.aarch64-pc-windows-msvc]
linker = "rust-lld"

View File

@@ -30,6 +30,8 @@ jobs:
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
with: with:
components: clippy components: clippy
- name: Cache Rust workspace
uses: Swatinem/rust-cache@v2
- name: Cargo check - name: Cargo check
run: cargo check --all-features --all-targets run: cargo check --all-features --all-targets
- name: Cargo clippy - name: Cargo clippy
@@ -85,6 +87,8 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Setup Rust toolchain - name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
- name: Cache Rust workspace
uses: Swatinem/rust-cache@v2
- name: Cargo test - name: Cargo test
run: cargo test --release --all-features run: cargo test --release --all-features
@@ -142,11 +146,19 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Install cargo-zigbuild - name: Install cargo-zigbuild
if: matrix.build == 'zigbuild' if: matrix.build == 'zigbuild'
run: pip install ziglang==0.13.0 cargo-zigbuild==0.19.1 run: |
python3 -m venv .venv
. .venv/bin/activate
echo PATH=$PATH >> $GITHUB_ENV
pip install ziglang==0.13.0 cargo-zigbuild==0.19.1
- name: Setup Rust toolchain - name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
with: with:
targets: ${{ matrix.target }} targets: ${{ matrix.target }}
- name: Cache Rust workspace
uses: Swatinem/rust-cache@v2
with:
key: ${{ matrix.target }}
- name: Cargo build - name: Cargo build
run: > run: >
cargo ${{ matrix.build }} --profile ${{ env.BUILD_PROFILE }} --target ${{ matrix.target }} cargo ${{ matrix.build }} --profile ${{ env.BUILD_PROFILE }} --target ${{ matrix.target }}
@@ -198,6 +210,10 @@ jobs:
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
with: with:
targets: ${{ matrix.target }} targets: ${{ matrix.target }}
- name: Cache Rust workspace
uses: Swatinem/rust-cache@v2
with:
key: ${{ matrix.target }}
- name: Cargo build - name: Cargo build
run: > run: >
cargo build --profile ${{ env.BUILD_PROFILE }} --target ${{ matrix.target }} cargo build --profile ${{ env.BUILD_PROFILE }} --target ${{ matrix.target }}

4
.gitignore vendored
View File

@@ -3,10 +3,6 @@ target/
**/*.rs.bk **/*.rs.bk
generated/ generated/
# cargo-mobile
.cargo/
/gen
# macOS # macOS
.DS_Store .DS_Store

40
Cargo.lock generated
View File

@@ -395,6 +395,15 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bimap"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "230c5f1ca6a325a32553f8640d31ac9b49f2411e901e427570154868b46da4f7"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "bit-set" name = "bit-set"
version = "0.6.0" version = "0.6.0"
@@ -1517,15 +1526,15 @@ dependencies = [
[[package]] [[package]]
name = "futures-core" name = "futures-core"
version = "0.3.30" version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]] [[package]]
name = "futures-io" name = "futures-io"
version = "0.3.30" version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
[[package]] [[package]]
name = "futures-lite" name = "futures-lite"
@@ -1542,9 +1551,9 @@ dependencies = [
[[package]] [[package]]
name = "futures-macro" name = "futures-macro"
version = "0.3.30" version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -1553,21 +1562,21 @@ dependencies = [
[[package]] [[package]]
name = "futures-sink" name = "futures-sink"
version = "0.3.30" version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
[[package]] [[package]]
name = "futures-task" name = "futures-task"
version = "0.3.30" version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
[[package]] [[package]]
name = "futures-util" name = "futures-util"
version = "0.3.30" version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-io", "futures-io",
@@ -2852,7 +2861,7 @@ dependencies = [
[[package]] [[package]]
name = "objdiff-cli" name = "objdiff-cli"
version = "2.2.2" version = "2.3.4"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"argp", "argp",
@@ -2874,10 +2883,11 @@ dependencies = [
[[package]] [[package]]
name = "objdiff-core" name = "objdiff-core"
version = "2.2.2" version = "2.3.4"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"arm-attr", "arm-attr",
"bimap",
"byteorder", "byteorder",
"console_error_panic_hook", "console_error_panic_hook",
"console_log", "console_log",
@@ -2913,7 +2923,7 @@ dependencies = [
[[package]] [[package]]
name = "objdiff-gui" name = "objdiff-gui"
version = "2.2.2" version = "2.3.4"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",

View File

@@ -13,7 +13,7 @@ strip = "debuginfo"
codegen-units = 1 codegen-units = 1
[workspace.package] [workspace.package]
version = "2.2.2" version = "2.3.4"
authors = ["Luke Street <luke@street.dev>"] authors = ["Luke Street <luke@street.dev>"]
edition = "2021" edition = "2021"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"

View File

@@ -133,6 +133,13 @@
}, },
"metadata": { "metadata": {
"ref": "#/$defs/metadata" "ref": "#/$defs/metadata"
},
"symbol_mappings": {
"type": "object",
"description": "Manual symbol mappings from target to base.",
"additionalProperties": {
"type": "string"
}
} }
} }
}, },

View File

@@ -70,7 +70,6 @@ feature-depth = 1
# A list of advisory IDs to ignore. Note that ignored advisories will still # A list of advisory IDs to ignore. Note that ignored advisories will still
# output a note when they are encountered. # output a note when they are encountered.
ignore = [ ignore = [
"RUSTSEC-2024-0370",
#{ id = "RUSTSEC-0000-0000", reason = "you can specify a reason the advisory is ignored" }, #{ id = "RUSTSEC-0000-0000", reason = "you can specify a reason the advisory is ignored" },
#"a-crate-that-is-yanked@0.1.1", # you can also ignore yanked crate versions if you wish #"a-crate-that-is-yanked@0.1.1", # you can also ignore yanked crate versions if you wish
#{ crate = "a-crate-that-is-yanked@0.1.1", reason = "you can specify why you are ignoring the yanked crate" }, #{ crate = "a-crate-that-is-yanked@0.1.1", reason = "you can specify why you are ignoring the yanked crate" },
@@ -240,7 +239,7 @@ allow-git = []
[sources.allow-org] [sources.allow-org]
# github.com organizations to allow git sources for # github.com organizations to allow git sources for
github = ["encounter"] github = ["notify-rs"]
# gitlab.com organizations to allow git sources for # gitlab.com organizations to allow git sources for
gitlab = [] gitlab = []
# bitbucket.org organizations to allow git sources for # bitbucket.org organizations to allow git sources for

View File

@@ -102,26 +102,32 @@ pub fn run(args: Args) -> Result<()> {
let unit_path = let unit_path =
PathBuf::from_str(u).ok().and_then(|p| fs::canonicalize(p).ok()); PathBuf::from_str(u).ok().and_then(|p| fs::canonicalize(p).ok());
let Some(object) = project_config.objects.iter_mut().find_map(|obj| { let Some(object) = project_config
if obj.name.as_deref() == Some(u) { .units
.as_deref_mut()
.unwrap_or_default()
.iter_mut()
.find_map(|obj| {
if obj.name.as_deref() == Some(u) {
resolve_paths(obj);
return Some(obj);
}
let up = unit_path.as_deref()?;
resolve_paths(obj); resolve_paths(obj);
return Some(obj);
}
let up = unit_path.as_deref()?; if [&obj.base_path, &obj.target_path]
.into_iter()
.filter_map(|p| p.as_ref().and_then(|p| p.canonicalize().ok()))
.any(|p| p == up)
{
return Some(obj);
}
resolve_paths(obj); None
})
if [&obj.base_path, &obj.target_path] else {
.into_iter()
.filter_map(|p| p.as_ref().and_then(|p| p.canonicalize().ok()))
.any(|p| p == up)
{
return Some(obj);
}
None
}) else {
bail!("Unit not found: {}", u) bail!("Unit not found: {}", u)
}; };
@@ -129,7 +135,13 @@ pub fn run(args: Args) -> Result<()> {
} else if let Some(symbol_name) = &args.symbol { } else if let Some(symbol_name) = &args.symbol {
let mut idx = None; let mut idx = None;
let mut count = 0usize; let mut count = 0usize;
for (i, obj) in project_config.objects.iter_mut().enumerate() { for (i, obj) in project_config
.units
.as_deref_mut()
.unwrap_or_default()
.iter_mut()
.enumerate()
{
resolve_paths(obj); resolve_paths(obj);
if obj if obj
@@ -148,7 +160,7 @@ pub fn run(args: Args) -> Result<()> {
} }
match (count, idx) { match (count, idx) {
(0, None) => bail!("Symbol not found: {}", symbol_name), (0, None) => bail!("Symbol not found: {}", symbol_name),
(1, Some(i)) => &mut project_config.objects[i], (1, Some(i)) => &mut project_config.units_mut()[i],
(2.., Some(_)) => bail!( (2.., Some(_)) => bail!(
"Multiple instances of {} were found, try specifying a unit", "Multiple instances of {} were found, try specifying a unit",
symbol_name symbol_name
@@ -303,7 +315,7 @@ fn find_function(obj: &ObjInfo, name: &str) -> Option<SymbolRef> {
None None
} }
#[allow(dead_code)] #[expect(dead_code)]
struct FunctionDiffUi { struct FunctionDiffUi {
relax_reloc_diffs: bool, relax_reloc_diffs: bool,
left_highlight: HighlightKind, left_highlight: HighlightKind,
@@ -758,7 +770,7 @@ impl FunctionDiffUi {
self.scroll_y += self.per_page / if half { 2 } else { 1 }; self.scroll_y += self.per_page / if half { 2 } else { 1 };
} }
#[allow(clippy::too_many_arguments)] #[expect(clippy::too_many_arguments)]
fn print_sym( fn print_sym(
&self, &self,
out: &mut Text<'static>, out: &mut Text<'static>,

View File

@@ -94,7 +94,7 @@ fn generate(args: GenerateArgs) -> Result<()> {
}; };
info!( info!(
"Generating report for {} units (using {} threads)", "Generating report for {} units (using {} threads)",
project.objects.len(), project.units().len(),
if args.deduplicate { 1 } else { rayon::current_num_threads() } if args.deduplicate { 1 } else { rayon::current_num_threads() }
); );
@@ -103,7 +103,7 @@ fn generate(args: GenerateArgs) -> Result<()> {
let mut existing_functions: HashSet<String> = HashSet::new(); let mut existing_functions: HashSet<String> = HashSet::new();
if args.deduplicate { if args.deduplicate {
// If deduplicating, we need to run single-threaded // If deduplicating, we need to run single-threaded
for object in &mut project.objects { for object in project.units.as_deref_mut().unwrap_or_default() {
if let Some(unit) = report_object( if let Some(unit) = report_object(
object, object,
project_dir, project_dir,
@@ -116,7 +116,9 @@ fn generate(args: GenerateArgs) -> Result<()> {
} }
} else { } else {
let vec = project let vec = project
.objects .units
.as_deref_mut()
.unwrap_or_default()
.par_iter_mut() .par_iter_mut()
.map(|object| { .map(|object| {
report_object( report_object(
@@ -132,7 +134,7 @@ fn generate(args: GenerateArgs) -> Result<()> {
} }
let measures = units.iter().flat_map(|u| u.measures.into_iter()).collect(); let measures = units.iter().flat_map(|u| u.measures.into_iter()).collect();
let mut categories = Vec::new(); let mut categories = Vec::new();
for category in &project.progress_categories { for category in project.progress_categories() {
categories.push(ReportCategory { categories.push(ReportCategory {
id: category.id.clone(), id: category.id.clone(),
name: category.name.clone(), name: category.name.clone(),

View File

@@ -17,35 +17,36 @@ crate-type = ["cdylib", "rlib"]
[features] [features]
all = ["config", "dwarf", "mips", "ppc", "x86", "arm", "bindings"] all = ["config", "dwarf", "mips", "ppc", "x86", "arm", "bindings"]
any-arch = [] # Implicit, used to check if any arch is enabled any-arch = ["config", "dep:bimap", "dep:strum", "dep:similar", "dep:flagset", "dep:log", "dep:memmap2", "dep:byteorder", "dep:num-traits"] # Implicit, used to check if any arch is enabled
config = ["globset", "semver", "serde_json", "serde_yaml"] config = ["dep:bimap", "dep:globset", "dep:semver", "dep:serde_json", "dep:serde_yaml", "dep:serde", "dep:filetime"]
dwarf = ["gimli"] dwarf = ["dep:gimli"]
mips = ["any-arch", "rabbitizer"] mips = ["any-arch", "dep:rabbitizer"]
ppc = ["any-arch", "cwdemangle", "cwextab", "ppc750cl"] ppc = ["any-arch", "dep:cwdemangle", "dep:cwextab", "dep:ppc750cl"]
x86 = ["any-arch", "cpp_demangle", "iced-x86", "msvc-demangler"] x86 = ["any-arch", "dep:cpp_demangle", "dep:iced-x86", "dep:msvc-demangler"]
arm = ["any-arch", "cpp_demangle", "unarm", "arm-attr"] arm = ["any-arch", "dep:cpp_demangle", "dep:unarm", "dep:arm-attr"]
bindings = ["serde_json", "prost", "pbjson"] bindings = ["dep:serde_json", "dep:prost", "dep:pbjson", "dep:serde", "dep:prost-build", "dep:pbjson-build"]
wasm = ["bindings", "console_error_panic_hook", "console_log"] wasm = ["bindings", "any-arch", "dep:console_error_panic_hook", "dep:console_log", "dep:wasm-bindgen", "dep:tsify-next", "dep:log"]
[package.metadata.docs.rs] [package.metadata.docs.rs]
features = ["all"] features = ["all"]
[dependencies] [dependencies]
anyhow = "1.0" anyhow = "1.0"
byteorder = "1.5" bimap = { version = "0.6", features = ["serde"], optional = true }
filetime = "0.2" byteorder = { version = "1.5", optional = true }
flagset = "0.4" filetime = { version = "0.2", optional = true }
log = "0.4" flagset = { version = "0.4", optional = true }
memmap2 = "0.9" log = { version = "0.4", optional = true }
num-traits = "0.2" memmap2 = { version = "0.9", optional = true }
num-traits = { version = "0.2", optional = true }
object = { version = "0.36", features = ["read_core", "std", "elf", "pe"], default-features = false } object = { version = "0.36", features = ["read_core", "std", "elf", "pe"], default-features = false }
pbjson = { version = "0.7", optional = true } pbjson = { version = "0.7", optional = true }
prost = { version = "0.13", optional = true } prost = { version = "0.13", optional = true }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"], optional = true }
similar = { version = "2.6", default-features = false } similar = { version = "2.6", default-features = false, optional = true }
strum = { version = "0.26", features = ["derive"] } strum = { version = "0.26", features = ["derive"], optional = true }
wasm-bindgen = "0.2" wasm-bindgen = { version = "0.2", optional = true }
tsify-next = { version = "0.5", default-features = false, features = ["js"] } tsify-next = { version = "0.5", default-features = false, features = ["js"], optional = true }
console_log = { version = "1.0", optional = true } console_log = { version = "1.0", optional = true }
console_error_panic_hook = { version = "0.1", optional = true } console_error_panic_hook = { version = "0.1", optional = true }
@@ -76,5 +77,5 @@ unarm = { version = "1.6", optional = true }
arm-attr = { version = "0.1", optional = true } arm-attr = { version = "0.1", optional = true }
[build-dependencies] [build-dependencies]
prost-build = "0.13" prost-build = { version = "0.13", optional = true }
pbjson-build = "0.7" pbjson-build = { version = "0.7", optional = true }

View File

@@ -1,6 +1,11 @@
use std::path::{Path, PathBuf};
fn main() { fn main() {
#[cfg(feature = "bindings")]
compile_protos();
}
#[cfg(feature = "bindings")]
fn compile_protos() {
use std::path::{Path, PathBuf};
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("protos"); let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("protos");
let descriptor_path = root.join("proto_descriptor.bin"); let descriptor_path = root.join("proto_descriptor.bin");
println!("cargo:rerun-if-changed={}", descriptor_path.display()); println!("cargo:rerun-if-changed={}", descriptor_path.display());

View File

@@ -83,7 +83,7 @@ impl ObjArch for ObjArchMips {
&self, &self,
address: u64, address: u64,
code: &[u8], code: &[u8],
_section_index: usize, section_index: usize,
relocations: &[ObjReloc], relocations: &[ObjReloc],
line_info: &BTreeMap<u64, u32>, line_info: &BTreeMap<u64, u32>,
config: &DiffObjConfig, config: &DiffObjConfig,
@@ -140,11 +140,18 @@ impl ObjArch for ObjArchMips {
| OperandType::cpu_label | OperandType::cpu_label
| OperandType::cpu_branch_target_label => { | OperandType::cpu_branch_target_label => {
if let Some(reloc) = reloc { if let Some(reloc) = reloc {
if matches!(&reloc.target_section, Some(s) if s == ".text") // If the relocation target is within the current function, we can
&& reloc.target.address > start_address // convert it into a relative branch target. Note that we check
&& reloc.target.address < end_address // target_address > start_address instead of >= so that recursive
// tail calls are not considered branch targets.
let target_address =
reloc.target.address.checked_add_signed(reloc.addend);
if reloc.target.orig_section_index == Some(section_index)
&& matches!(target_address, Some(addr) if addr > start_address && addr < end_address)
{ {
args.push(ObjInsArg::BranchDest(reloc.target.address)); let target_address = target_address.unwrap();
args.push(ObjInsArg::BranchDest(target_address));
branch_dest = Some(target_address);
} else { } else {
push_reloc(&mut args, reloc)?; push_reloc(&mut args, reloc)?;
branch_dest = None; branch_dest = None;

View File

@@ -34,7 +34,11 @@ pub enum DataType {
impl DataType { impl DataType {
pub fn display_bytes<Endian: ByteOrder>(&self, bytes: &[u8]) -> Option<String> { pub fn display_bytes<Endian: ByteOrder>(&self, bytes: &[u8]) -> Option<String> {
if self.required_len().is_some_and(|l| bytes.len() < l) { // TODO: Attempt to interpret large symbols as arrays of a smaller type,
// fallback to intrepreting it as bytes.
// https://github.com/encounter/objdiff/issues/124
if self.required_len().is_some_and(|l| bytes.len() != l) {
log::warn!("Failed to display a symbol value for a symbol whose size doesn't match the instruction referencing it.");
return None; return None;
} }

View File

@@ -70,9 +70,13 @@ impl FunctionDiff {
// let (_section, symbol) = object.section_symbol(symbol_ref); // let (_section, symbol) = object.section_symbol(symbol_ref);
// Symbol::from(symbol) // Symbol::from(symbol)
// }); // });
let instructions = symbol_diff.instructions.iter().map(InstructionDiff::from).collect(); let instructions = symbol_diff
.instructions
.iter()
.map(|ins_diff| InstructionDiff::new(object, ins_diff))
.collect();
Self { Self {
symbol: Some(Symbol::from(symbol)), symbol: Some(Symbol::new(symbol)),
// diff_symbol, // diff_symbol,
instructions, instructions,
match_percent: symbol_diff.match_percent, match_percent: symbol_diff.match_percent,
@@ -90,8 +94,8 @@ impl DataDiff {
} }
} }
impl<'a> From<&'a ObjSymbol> for Symbol { impl Symbol {
fn from(value: &'a ObjSymbol) -> Self { pub fn new(value: &ObjSymbol) -> Self {
Self { Self {
name: value.name.to_string(), name: value.name.to_string(),
demangled_name: value.demangled_name.clone(), demangled_name: value.demangled_name.clone(),
@@ -122,29 +126,29 @@ fn symbol_flags(value: ObjSymbolFlagSet) -> u32 {
flags flags
} }
impl<'a> From<&'a ObjIns> for Instruction { impl Instruction {
fn from(value: &'a ObjIns) -> Self { pub fn new(object: &ObjInfo, instruction: &ObjIns) -> Self {
Self { Self {
address: value.address, address: instruction.address,
size: value.size as u32, size: instruction.size as u32,
opcode: value.op as u32, opcode: instruction.op as u32,
mnemonic: value.mnemonic.clone(), mnemonic: instruction.mnemonic.clone(),
formatted: value.formatted.clone(), formatted: instruction.formatted.clone(),
arguments: value.args.iter().map(Argument::from).collect(), arguments: instruction.args.iter().map(Argument::new).collect(),
relocation: value.reloc.as_ref().map(Relocation::from), relocation: instruction.reloc.as_ref().map(|reloc| Relocation::new(object, reloc)),
branch_dest: value.branch_dest, branch_dest: instruction.branch_dest,
line_number: value.line, line_number: instruction.line,
original: value.orig.clone(), original: instruction.orig.clone(),
} }
} }
} }
impl<'a> From<&'a ObjInsArg> for Argument { impl Argument {
fn from(value: &'a ObjInsArg) -> Self { pub fn new(value: &ObjInsArg) -> Self {
Self { Self {
value: Some(match value { value: Some(match value {
ObjInsArg::PlainText(s) => argument::Value::PlainText(s.to_string()), ObjInsArg::PlainText(s) => argument::Value::PlainText(s.to_string()),
ObjInsArg::Arg(v) => argument::Value::Argument(ArgumentValue::from(v)), ObjInsArg::Arg(v) => argument::Value::Argument(ArgumentValue::new(v)),
ObjInsArg::Reloc => argument::Value::Relocation(ArgumentRelocation {}), ObjInsArg::Reloc => argument::Value::Relocation(ArgumentRelocation {}),
ObjInsArg::BranchDest(dest) => argument::Value::BranchDest(*dest), ObjInsArg::BranchDest(dest) => argument::Value::BranchDest(*dest),
}), }),
@@ -152,8 +156,8 @@ impl<'a> From<&'a ObjInsArg> for Argument {
} }
} }
impl From<&ObjInsArgValue> for ArgumentValue { impl ArgumentValue {
fn from(value: &ObjInsArgValue) -> Self { pub fn new(value: &ObjInsArgValue) -> Self {
Self { Self {
value: Some(match value { value: Some(match value {
ObjInsArgValue::Signed(v) => argument_value::Value::Signed(*v), ObjInsArgValue::Signed(v) => argument_value::Value::Signed(*v),
@@ -164,42 +168,39 @@ impl From<&ObjInsArgValue> for ArgumentValue {
} }
} }
impl<'a> From<&'a ObjReloc> for Relocation { impl Relocation {
fn from(value: &ObjReloc) -> Self { pub fn new(object: &ObjInfo, reloc: &ObjReloc) -> Self {
Self { Self {
r#type: match value.flags { r#type: match reloc.flags {
object::RelocationFlags::Elf { r_type } => r_type, object::RelocationFlags::Elf { r_type } => r_type,
object::RelocationFlags::MachO { r_type, .. } => r_type as u32, object::RelocationFlags::MachO { r_type, .. } => r_type as u32,
object::RelocationFlags::Coff { typ } => typ as u32, object::RelocationFlags::Coff { typ } => typ as u32,
object::RelocationFlags::Xcoff { r_rtype, .. } => r_rtype as u32, object::RelocationFlags::Xcoff { r_rtype, .. } => r_rtype as u32,
_ => unreachable!(), _ => unreachable!(),
}, },
type_name: String::new(), // TODO type_name: object.arch.display_reloc(reloc.flags).into_owned(),
target: Some(RelocationTarget::from(&value.target)), target: Some(RelocationTarget {
symbol: Some(Symbol::new(&reloc.target)),
addend: reloc.addend,
}),
} }
} }
} }
impl<'a> From<&'a ObjSymbol> for RelocationTarget { impl InstructionDiff {
fn from(value: &'a ObjSymbol) -> Self { pub fn new(object: &ObjInfo, instruction_diff: &ObjInsDiff) -> Self {
Self { symbol: Some(Symbol::from(value)), addend: value.addend }
}
}
impl<'a> From<&'a ObjInsDiff> for InstructionDiff {
fn from(value: &'a ObjInsDiff) -> Self {
Self { Self {
instruction: value.ins.as_ref().map(Instruction::from), instruction: instruction_diff.ins.as_ref().map(|ins| Instruction::new(object, ins)),
diff_kind: DiffKind::from(value.kind) as i32, diff_kind: DiffKind::from(instruction_diff.kind) as i32,
branch_from: value.branch_from.as_ref().map(InstructionBranchFrom::from), branch_from: instruction_diff.branch_from.as_ref().map(InstructionBranchFrom::new),
branch_to: value.branch_to.as_ref().map(InstructionBranchTo::from), branch_to: instruction_diff.branch_to.as_ref().map(InstructionBranchTo::new),
arg_diff: value.arg_diff.iter().map(ArgumentDiff::from).collect(), arg_diff: instruction_diff.arg_diff.iter().map(ArgumentDiff::new).collect(),
} }
} }
} }
impl From<&Option<ObjInsArgDiff>> for ArgumentDiff { impl ArgumentDiff {
fn from(value: &Option<ObjInsArgDiff>) -> Self { pub fn new(value: &Option<ObjInsArgDiff>) -> Self {
Self { diff_index: value.as_ref().map(|v| v.idx as u32) } Self { diff_index: value.as_ref().map(|v| v.idx as u32) }
} }
} }
@@ -228,8 +229,8 @@ impl From<ObjDataDiffKind> for DiffKind {
} }
} }
impl<'a> From<&'a ObjInsBranchFrom> for InstructionBranchFrom { impl InstructionBranchFrom {
fn from(value: &'a ObjInsBranchFrom) -> Self { pub fn new(value: &ObjInsBranchFrom) -> Self {
Self { Self {
instruction_index: value.ins_idx.iter().map(|&x| x as u32).collect(), instruction_index: value.ins_idx.iter().map(|&x| x as u32).collect(),
branch_index: value.branch_idx as u32, branch_index: value.branch_idx as u32,
@@ -237,8 +238,8 @@ impl<'a> From<&'a ObjInsBranchFrom> for InstructionBranchFrom {
} }
} }
impl<'a> From<&'a ObjInsBranchTo> for InstructionBranchTo { impl InstructionBranchTo {
fn from(value: &'a ObjInsBranchTo) -> Self { pub fn new(value: &ObjInsBranchTo) -> Self {
Self { instruction_index: value.ins_idx as u32, branch_index: value.branch_idx as u32 } Self { instruction_index: value.ins_idx as u32, branch_index: value.branch_idx as u32 }
} }
} }

View File

@@ -1,77 +1,100 @@
use std::{ use std::{
fs,
fs::File, fs::File,
io::{BufReader, Read}, io::{BufReader, BufWriter, Read},
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use bimap::BiBTreeMap;
use filetime::FileTime; use filetime::FileTime;
use globset::{Glob, GlobSet, GlobSetBuilder}; use globset::{Glob, GlobSet, GlobSetBuilder};
#[inline] #[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
fn bool_true() -> bool { true }
#[derive(Default, Clone, serde::Deserialize)]
pub struct ProjectConfig { pub struct ProjectConfig {
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub min_version: Option<String>, pub min_version: Option<String>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub custom_make: Option<String>, pub custom_make: Option<String>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub custom_args: Option<Vec<String>>, pub custom_args: Option<Vec<String>>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub target_dir: Option<PathBuf>, pub target_dir: Option<PathBuf>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub base_dir: Option<PathBuf>, pub base_dir: Option<PathBuf>,
#[serde(default = "bool_true")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub build_base: bool, pub build_base: Option<bool>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub build_target: bool, pub build_target: Option<bool>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub watch_patterns: Option<Vec<Glob>>, pub watch_patterns: Option<Vec<Glob>>,
#[serde(default, alias = "units")] #[serde(default, alias = "objects", skip_serializing_if = "Option::is_none")]
pub objects: Vec<ProjectObject>, pub units: Option<Vec<ProjectObject>>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub progress_categories: Vec<ProjectProgressCategory>, pub progress_categories: Option<Vec<ProjectProgressCategory>>,
} }
#[derive(Default, Clone, serde::Deserialize)] impl ProjectConfig {
#[inline]
pub fn units(&self) -> &[ProjectObject] { self.units.as_deref().unwrap_or_default() }
#[inline]
pub fn units_mut(&mut self) -> &mut Vec<ProjectObject> {
self.units.get_or_insert_with(Vec::new)
}
#[inline]
pub fn progress_categories(&self) -> &[ProjectProgressCategory] {
self.progress_categories.as_deref().unwrap_or_default()
}
#[inline]
pub fn progress_categories_mut(&mut self) -> &mut Vec<ProjectProgressCategory> {
self.progress_categories.get_or_insert_with(Vec::new)
}
}
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
pub struct ProjectObject { pub struct ProjectObject {
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>, pub name: Option<String>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub path: Option<PathBuf>, pub path: Option<PathBuf>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub target_path: Option<PathBuf>, pub target_path: Option<PathBuf>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub base_path: Option<PathBuf>, pub base_path: Option<PathBuf>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
#[deprecated(note = "Use metadata.reverse_fn_order")] #[deprecated(note = "Use metadata.reverse_fn_order")]
pub reverse_fn_order: Option<bool>, pub reverse_fn_order: Option<bool>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
#[deprecated(note = "Use metadata.complete")] #[deprecated(note = "Use metadata.complete")]
pub complete: Option<bool>, pub complete: Option<bool>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub scratch: Option<ScratchConfig>, pub scratch: Option<ScratchConfig>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<ProjectObjectMetadata>, pub metadata: Option<ProjectObjectMetadata>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub symbol_mappings: Option<SymbolMappings>,
} }
#[derive(Default, Clone, serde::Deserialize)] pub type SymbolMappings = BiBTreeMap<String, String>;
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
pub struct ProjectObjectMetadata { pub struct ProjectObjectMetadata {
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub complete: Option<bool>, pub complete: Option<bool>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub reverse_fn_order: Option<bool>, pub reverse_fn_order: Option<bool>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub source_path: Option<String>, pub source_path: Option<String>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub progress_categories: Option<Vec<String>>, pub progress_categories: Option<Vec<String>>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub auto_generated: Option<bool>, pub auto_generated: Option<bool>,
} }
#[derive(Default, Clone, serde::Deserialize)] #[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
pub struct ProjectProgressCategory { pub struct ProjectProgressCategory {
#[serde(default)] #[serde(default)]
pub id: String, pub id: String,
@@ -112,12 +135,12 @@ impl ProjectObject {
} }
pub fn complete(&self) -> Option<bool> { pub fn complete(&self) -> Option<bool> {
#[allow(deprecated)] #[expect(deprecated)]
self.metadata.as_ref().and_then(|m| m.complete).or(self.complete) self.metadata.as_ref().and_then(|m| m.complete).or(self.complete)
} }
pub fn reverse_fn_order(&self) -> Option<bool> { pub fn reverse_fn_order(&self) -> Option<bool> {
#[allow(deprecated)] #[expect(deprecated)]
self.metadata.as_ref().and_then(|m| m.reverse_fn_order).or(self.reverse_fn_order) self.metadata.as_ref().and_then(|m| m.reverse_fn_order).or(self.reverse_fn_order)
} }
@@ -132,16 +155,16 @@ impl ProjectObject {
#[derive(Default, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)] #[derive(Default, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct ScratchConfig { pub struct ScratchConfig {
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub platform: Option<String>, pub platform: Option<String>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub compiler: Option<String>, pub compiler: Option<String>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub c_flags: Option<String>, pub c_flags: Option<String>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub ctx_path: Option<PathBuf>, pub ctx_path: Option<PathBuf>,
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub build_ctx: bool, pub build_ctx: Option<bool>,
} }
pub const CONFIG_FILENAMES: [&str; 3] = ["objdiff.json", "objdiff.yml", "objdiff.yaml"]; pub const CONFIG_FILENAMES: [&str; 3] = ["objdiff.json", "objdiff.yml", "objdiff.yaml"];
@@ -154,7 +177,7 @@ pub const DEFAULT_WATCH_PATTERNS: &[&str] = &[
#[derive(Clone, Eq, PartialEq)] #[derive(Clone, Eq, PartialEq)]
pub struct ProjectConfigInfo { pub struct ProjectConfigInfo {
pub path: PathBuf, pub path: PathBuf,
pub timestamp: FileTime, pub timestamp: Option<FileTime>,
} }
pub fn try_project_config(dir: &Path) -> Option<(Result<ProjectConfig>, ProjectConfigInfo)> { pub fn try_project_config(dir: &Path) -> Option<(Result<ProjectConfig>, ProjectConfigInfo)> {
@@ -180,12 +203,41 @@ pub fn try_project_config(dir: &Path) -> Option<(Result<ProjectConfig>, ProjectC
result = Err(e); result = Err(e);
} }
} }
return Some((result, ProjectConfigInfo { path: config_path, timestamp: ts })); return Some((result, ProjectConfigInfo { path: config_path, timestamp: Some(ts) }));
} }
} }
None None
} }
pub fn save_project_config(
config: &ProjectConfig,
info: &ProjectConfigInfo,
) -> Result<ProjectConfigInfo> {
if let Some(last_ts) = info.timestamp {
// Check if the file has changed since we last read it
if let Ok(metadata) = fs::metadata(&info.path) {
let ts = FileTime::from_last_modification_time(&metadata);
if ts != last_ts {
return Err(anyhow!("Config file has changed since last read"));
}
}
}
let mut writer =
BufWriter::new(File::create(&info.path).context("Failed to create config file")?);
let ext = info.path.extension().and_then(|ext| ext.to_str()).unwrap_or("json");
match ext {
"json" => serde_json::to_writer_pretty(&mut writer, config).context("Failed to write JSON"),
"yml" | "yaml" => {
serde_yaml::to_writer(&mut writer, config).context("Failed to write YAML")
}
_ => Err(anyhow!("Unknown config file extension: {ext}")),
}?;
let file = writer.into_inner().context("Failed to flush file")?;
let metadata = file.metadata().context("Failed to get file metadata")?;
let ts = FileTime::from_last_modification_time(&metadata);
Ok(ProjectConfigInfo { path: info.path.clone(), timestamp: Some(ts) })
}
fn validate_min_version(config: &ProjectConfig) -> Result<()> { fn validate_min_version(config: &ProjectConfig) -> Result<()> {
let Some(min_version) = &config.min_version else { return Ok(()) }; let Some(min_version) = &config.min_version else { return Ok(()) };
let version = semver::Version::parse(env!("CARGO_PKG_VERSION")) let version = semver::Version::parse(env!("CARGO_PKG_VERSION"))

View File

@@ -9,7 +9,7 @@ use crate::{
DiffObjConfig, ObjInsArgDiff, ObjInsBranchFrom, ObjInsBranchTo, ObjInsDiff, ObjInsDiffKind, DiffObjConfig, ObjInsArgDiff, ObjInsBranchFrom, ObjInsBranchTo, ObjInsDiff, ObjInsDiffKind,
ObjSymbolDiff, ObjSymbolDiff,
}, },
obj::{ObjInfo, ObjInsArg, ObjReloc, ObjSymbol, ObjSymbolFlags, SymbolRef}, obj::{ObjInfo, ObjInsArg, ObjReloc, ObjSymbolFlags, SymbolRef},
}; };
pub fn process_code_symbol( pub fn process_code_symbol(
@@ -41,10 +41,12 @@ pub fn no_diff_code(out: &ProcessCodeResult, symbol_ref: SymbolRef) -> Result<Ob
}); });
} }
resolve_branches(&mut diff); resolve_branches(&mut diff);
Ok(ObjSymbolDiff { symbol_ref, diff_symbol: None, instructions: diff, match_percent: None }) Ok(ObjSymbolDiff { symbol_ref, target_symbol: None, instructions: diff, match_percent: None })
} }
pub fn diff_code( pub fn diff_code(
left_obj: &ObjInfo,
right_obj: &ObjInfo,
left_out: &ProcessCodeResult, left_out: &ProcessCodeResult,
right_out: &ProcessCodeResult, right_out: &ProcessCodeResult,
left_symbol_ref: SymbolRef, left_symbol_ref: SymbolRef,
@@ -60,14 +62,14 @@ pub fn diff_code(
let mut diff_state = InsDiffState::default(); let mut diff_state = InsDiffState::default();
for (left, right) in left_diff.iter_mut().zip(right_diff.iter_mut()) { for (left, right) in left_diff.iter_mut().zip(right_diff.iter_mut()) {
let result = compare_ins(config, left, right, &mut diff_state)?; let result = compare_ins(config, left_obj, right_obj, left, right, &mut diff_state)?;
left.kind = result.kind; left.kind = result.kind;
right.kind = result.kind; right.kind = result.kind;
left.arg_diff = result.left_args_diff; left.arg_diff = result.left_args_diff;
right.arg_diff = result.right_args_diff; right.arg_diff = result.right_args_diff;
} }
let total = left_out.insts.len(); let total = left_out.insts.len().max(right_out.insts.len());
let percent = if diff_state.diff_count >= total { let percent = if diff_state.diff_count >= total {
0.0 0.0
} else { } else {
@@ -77,13 +79,13 @@ pub fn diff_code(
Ok(( Ok((
ObjSymbolDiff { ObjSymbolDiff {
symbol_ref: left_symbol_ref, symbol_ref: left_symbol_ref,
diff_symbol: Some(right_symbol_ref), target_symbol: Some(right_symbol_ref),
instructions: left_diff, instructions: left_diff,
match_percent: Some(percent), match_percent: Some(percent),
}, },
ObjSymbolDiff { ObjSymbolDiff {
symbol_ref: right_symbol_ref, symbol_ref: right_symbol_ref,
diff_symbol: Some(left_symbol_ref), target_symbol: Some(left_symbol_ref),
instructions: right_diff, instructions: right_diff,
match_percent: Some(percent), match_percent: Some(percent),
}, },
@@ -170,12 +172,33 @@ fn resolve_branches(vec: &mut [ObjInsDiff]) {
} }
} }
fn address_eq(left: &ObjSymbol, right: &ObjSymbol) -> bool { fn address_eq(left: &ObjReloc, right: &ObjReloc) -> bool {
left.address as i64 + left.addend == right.address as i64 + right.addend left.target.address as i64 + left.addend == right.target.address as i64 + right.addend
}
fn section_name_eq(
left_obj: &ObjInfo,
right_obj: &ObjInfo,
left_orig_section_index: usize,
right_orig_section_index: usize,
) -> bool {
let Some(left_section) =
left_obj.sections.iter().find(|s| s.orig_index == left_orig_section_index)
else {
return false;
};
let Some(right_section) =
right_obj.sections.iter().find(|s| s.orig_index == right_orig_section_index)
else {
return false;
};
left_section.name == right_section.name
} }
fn reloc_eq( fn reloc_eq(
config: &DiffObjConfig, config: &DiffObjConfig,
left_obj: &ObjInfo,
right_obj: &ObjInfo,
left_reloc: Option<&ObjReloc>, left_reloc: Option<&ObjReloc>,
right_reloc: Option<&ObjReloc>, right_reloc: Option<&ObjReloc>,
) -> bool { ) -> bool {
@@ -189,29 +212,32 @@ fn reloc_eq(
return true; return true;
} }
let name_matches = left.target.name == right.target.name; let symbol_name_matches = left.target.name == right.target.name;
match (&left.target_section, &right.target_section) { match (&left.target.orig_section_index, &right.target.orig_section_index) {
(Some(sl), Some(sr)) => { (Some(sl), Some(sr)) => {
// Match if section and name or address match // Match if section and name or address match
sl == sr && (name_matches || address_eq(&left.target, &right.target)) section_name_eq(left_obj, right_obj, *sl, *sr)
&& (symbol_name_matches || address_eq(left, right))
} }
(Some(_), None) => false, (Some(_), None) => false,
(None, Some(_)) => { (None, Some(_)) => {
// Match if possibly stripped weak symbol // Match if possibly stripped weak symbol
name_matches && right.target.flags.0.contains(ObjSymbolFlags::Weak) symbol_name_matches && right.target.flags.0.contains(ObjSymbolFlags::Weak)
} }
(None, None) => name_matches, (None, None) => symbol_name_matches,
} }
} }
fn arg_eq( fn arg_eq(
config: &DiffObjConfig, config: &DiffObjConfig,
left_obj: &ObjInfo,
right_obj: &ObjInfo,
left: &ObjInsArg, left: &ObjInsArg,
right: &ObjInsArg, right: &ObjInsArg,
left_diff: &ObjInsDiff, left_diff: &ObjInsDiff,
right_diff: &ObjInsDiff, right_diff: &ObjInsDiff,
) -> bool { ) -> bool {
return match left { match left {
ObjInsArg::PlainText(l) => match right { ObjInsArg::PlainText(l) => match right {
ObjInsArg::PlainText(r) => l == r, ObjInsArg::PlainText(r) => l == r,
_ => false, _ => false,
@@ -227,6 +253,8 @@ fn arg_eq(
matches!(right, ObjInsArg::Reloc) matches!(right, ObjInsArg::Reloc)
&& reloc_eq( && reloc_eq(
config, config,
left_obj,
right_obj,
left_diff.ins.as_ref().and_then(|i| i.reloc.as_ref()), left_diff.ins.as_ref().and_then(|i| i.reloc.as_ref()),
right_diff.ins.as_ref().and_then(|i| i.reloc.as_ref()), right_diff.ins.as_ref().and_then(|i| i.reloc.as_ref()),
) )
@@ -236,7 +264,7 @@ fn arg_eq(
left_diff.branch_to.as_ref().map(|b| b.ins_idx) left_diff.branch_to.as_ref().map(|b| b.ins_idx)
== right_diff.branch_to.as_ref().map(|b| b.ins_idx) == right_diff.branch_to.as_ref().map(|b| b.ins_idx)
} }
}; }
} }
#[derive(Default)] #[derive(Default)]
@@ -257,6 +285,8 @@ struct InsDiffResult {
fn compare_ins( fn compare_ins(
config: &DiffObjConfig, config: &DiffObjConfig,
left_obj: &ObjInfo,
right_obj: &ObjInfo,
left: &ObjInsDiff, left: &ObjInsDiff,
right: &ObjInsDiff, right: &ObjInsDiff,
state: &mut InsDiffState, state: &mut InsDiffState,
@@ -283,7 +313,7 @@ fn compare_ins(
state.diff_count += 1; state.diff_count += 1;
} }
for (a, b) in left_ins.args.iter().zip(&right_ins.args) { for (a, b) in left_ins.args.iter().zip(&right_ins.args) {
if arg_eq(config, a, b, left, right) { if arg_eq(config, left_obj, right_obj, a, b, left, right) {
result.left_args_diff.push(None); result.left_args_diff.push(None);
result.right_args_diff.push(None); result.right_args_diff.push(None);
} else { } else {

View File

@@ -20,13 +20,13 @@ pub fn diff_bss_symbol(
Ok(( Ok((
ObjSymbolDiff { ObjSymbolDiff {
symbol_ref: left_symbol_ref, symbol_ref: left_symbol_ref,
diff_symbol: Some(right_symbol_ref), target_symbol: Some(right_symbol_ref),
instructions: vec![], instructions: vec![],
match_percent: Some(percent), match_percent: Some(percent),
}, },
ObjSymbolDiff { ObjSymbolDiff {
symbol_ref: right_symbol_ref, symbol_ref: right_symbol_ref,
diff_symbol: Some(left_symbol_ref), target_symbol: Some(left_symbol_ref),
instructions: vec![], instructions: vec![],
match_percent: Some(percent), match_percent: Some(percent),
}, },
@@ -34,7 +34,7 @@ pub fn diff_bss_symbol(
} }
pub fn no_diff_symbol(_obj: &ObjInfo, symbol_ref: SymbolRef) -> ObjSymbolDiff { pub fn no_diff_symbol(_obj: &ObjInfo, symbol_ref: SymbolRef) -> ObjSymbolDiff {
ObjSymbolDiff { symbol_ref, diff_symbol: None, instructions: vec![], match_percent: None } ObjSymbolDiff { symbol_ref, target_symbol: None, instructions: vec![], match_percent: None }
} }
/// Compare the data sections of two object files. /// Compare the data sections of two object files.
@@ -158,13 +158,13 @@ pub fn diff_data_symbol(
Ok(( Ok((
ObjSymbolDiff { ObjSymbolDiff {
symbol_ref: left_symbol_ref, symbol_ref: left_symbol_ref,
diff_symbol: Some(right_symbol_ref), target_symbol: Some(right_symbol_ref),
instructions: vec![], instructions: vec![],
match_percent: Some(match_percent), match_percent: Some(match_percent),
}, },
ObjSymbolDiff { ObjSymbolDiff {
symbol_ref: right_symbol_ref, symbol_ref: right_symbol_ref,
diff_symbol: Some(left_symbol_ref), target_symbol: Some(left_symbol_ref),
instructions: vec![], instructions: vec![],
match_percent: Some(match_percent), match_percent: Some(match_percent),
}, },

View File

@@ -29,7 +29,7 @@ pub enum DiffText<'a> {
Eol, Eol,
} }
#[derive(Default, Clone, PartialEq, Eq)] #[derive(Debug, Default, Clone, PartialEq, Eq)]
pub enum HighlightKind { pub enum HighlightKind {
#[default] #[default]
None, None,
@@ -94,9 +94,9 @@ fn display_reloc_name<E>(
mut cb: impl FnMut(DiffText) -> Result<(), E>, mut cb: impl FnMut(DiffText) -> Result<(), E>,
) -> Result<(), E> { ) -> Result<(), E> {
cb(DiffText::Symbol(&reloc.target))?; cb(DiffText::Symbol(&reloc.target))?;
match reloc.target.addend.cmp(&0i64) { match reloc.addend.cmp(&0i64) {
Ordering::Greater => cb(DiffText::Basic(&format!("+{:#x}", reloc.target.addend))), Ordering::Greater => cb(DiffText::Basic(&format!("+{:#x}", reloc.addend))),
Ordering::Less => cb(DiffText::Basic(&format!("-{:#x}", -reloc.target.addend))), Ordering::Less => cb(DiffText::Basic(&format!("-{:#x}", -reloc.addend))),
_ => Ok(()), _ => Ok(()),
} }
} }

View File

@@ -3,6 +3,7 @@ use std::collections::HashSet;
use anyhow::Result; use anyhow::Result;
use crate::{ use crate::{
config::SymbolMappings,
diff::{ diff::{
code::{diff_code, no_diff_code, process_code_symbol}, code::{diff_code, no_diff_code, process_code_symbol},
data::{ data::{
@@ -10,7 +11,7 @@ use crate::{
diff_generic_section, no_diff_symbol, diff_generic_section, no_diff_symbol,
}, },
}, },
obj::{ObjInfo, ObjIns, ObjSection, ObjSectionKind, ObjSymbol, SymbolRef}, obj::{ObjInfo, ObjIns, ObjSection, ObjSectionKind, ObjSymbol, SymbolRef, SECTION_COMMON},
}; };
pub mod code; pub mod code;
@@ -28,8 +29,8 @@ pub mod display;
serde::Serialize, serde::Serialize,
strum::VariantArray, strum::VariantArray,
strum::EnumMessage, strum::EnumMessage,
tsify_next::Tsify,
)] )]
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
pub enum X86Formatter { pub enum X86Formatter {
#[default] #[default]
#[strum(message = "Intel (default)")] #[strum(message = "Intel (default)")]
@@ -53,8 +54,8 @@ pub enum X86Formatter {
serde::Serialize, serde::Serialize,
strum::VariantArray, strum::VariantArray,
strum::EnumMessage, strum::EnumMessage,
tsify_next::Tsify,
)] )]
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
pub enum MipsAbi { pub enum MipsAbi {
#[default] #[default]
#[strum(message = "Auto (default)")] #[strum(message = "Auto (default)")]
@@ -78,8 +79,8 @@ pub enum MipsAbi {
serde::Serialize, serde::Serialize,
strum::VariantArray, strum::VariantArray,
strum::EnumMessage, strum::EnumMessage,
tsify_next::Tsify,
)] )]
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
pub enum MipsInstrCategory { pub enum MipsInstrCategory {
#[default] #[default]
#[strum(message = "Auto (default)")] #[strum(message = "Auto (default)")]
@@ -107,8 +108,8 @@ pub enum MipsInstrCategory {
serde::Serialize, serde::Serialize,
strum::VariantArray, strum::VariantArray,
strum::EnumMessage, strum::EnumMessage,
tsify_next::Tsify,
)] )]
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
pub enum ArmArchVersion { pub enum ArmArchVersion {
#[default] #[default]
#[strum(message = "Auto (default)")] #[strum(message = "Auto (default)")]
@@ -132,8 +133,8 @@ pub enum ArmArchVersion {
serde::Serialize, serde::Serialize,
strum::VariantArray, strum::VariantArray,
strum::EnumMessage, strum::EnumMessage,
tsify_next::Tsify,
)] )]
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
pub enum ArmR9Usage { pub enum ArmR9Usage {
#[default] #[default]
#[strum( #[strum(
@@ -153,14 +154,17 @@ pub enum ArmR9Usage {
#[inline] #[inline]
const fn default_true() -> bool { true } const fn default_true() -> bool { true }
#[derive(Debug, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize, tsify_next::Tsify)] #[derive(Debug, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[tsify(from_wasm_abi)] #[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
#[cfg_attr(feature = "wasm", tsify(from_wasm_abi))]
#[serde(default)] #[serde(default)]
pub struct DiffObjConfig { pub struct DiffObjConfig {
pub relax_reloc_diffs: bool, pub relax_reloc_diffs: bool,
#[serde(default = "default_true")] #[serde(default = "default_true")]
pub space_between_args: bool, pub space_between_args: bool,
pub combine_data_sections: bool, pub combine_data_sections: bool,
#[serde(default)]
pub symbol_mappings: MappingConfig,
// x86 // x86
pub x86_formatter: X86Formatter, pub x86_formatter: X86Formatter,
// MIPS // MIPS
@@ -182,6 +186,7 @@ impl Default for DiffObjConfig {
relax_reloc_diffs: false, relax_reloc_diffs: false,
space_between_args: true, space_between_args: true,
combine_data_sections: false, combine_data_sections: false,
symbol_mappings: Default::default(),
x86_formatter: Default::default(), x86_formatter: Default::default(),
mips_abi: Default::default(), mips_abi: Default::default(),
mips_instr_category: Default::default(), mips_instr_category: Default::default(),
@@ -223,8 +228,10 @@ impl ObjSectionDiff {
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct ObjSymbolDiff { pub struct ObjSymbolDiff {
/// The symbol ref this object
pub symbol_ref: SymbolRef, pub symbol_ref: SymbolRef,
pub diff_symbol: Option<SymbolRef>, /// The symbol ref in the _other_ object that this symbol was diffed against
pub target_symbol: Option<SymbolRef>,
pub instructions: Vec<ObjInsDiff>, pub instructions: Vec<ObjInsDiff>,
pub match_percent: Option<f32>, pub match_percent: Option<f32>,
} }
@@ -294,8 +301,13 @@ pub struct ObjInsBranchTo {
#[derive(Default)] #[derive(Default)]
pub struct ObjDiff { pub struct ObjDiff {
/// A list of all section diffs in the object.
pub sections: Vec<ObjSectionDiff>, pub sections: Vec<ObjSectionDiff>,
/// Common BSS symbols don't live in a section, so they're stored separately.
pub common: Vec<ObjSymbolDiff>, pub common: Vec<ObjSymbolDiff>,
/// If `selecting_left` or `selecting_right` is set, this is the list of symbols
/// that are being mapped to the other object.
pub mapping_symbols: Vec<ObjSymbolDiff>,
} }
impl ObjDiff { impl ObjDiff {
@@ -303,13 +315,14 @@ impl ObjDiff {
let mut result = Self { let mut result = Self {
sections: Vec::with_capacity(obj.sections.len()), sections: Vec::with_capacity(obj.sections.len()),
common: Vec::with_capacity(obj.common.len()), common: Vec::with_capacity(obj.common.len()),
mapping_symbols: vec![],
}; };
for (section_idx, section) in obj.sections.iter().enumerate() { for (section_idx, section) in obj.sections.iter().enumerate() {
let mut symbols = Vec::with_capacity(section.symbols.len()); let mut symbols = Vec::with_capacity(section.symbols.len());
for (symbol_idx, _) in section.symbols.iter().enumerate() { for (symbol_idx, _) in section.symbols.iter().enumerate() {
symbols.push(ObjSymbolDiff { symbols.push(ObjSymbolDiff {
symbol_ref: SymbolRef { section_idx, symbol_idx }, symbol_ref: SymbolRef { section_idx, symbol_idx },
diff_symbol: None, target_symbol: None,
instructions: vec![], instructions: vec![],
match_percent: None, match_percent: None,
}); });
@@ -327,8 +340,8 @@ impl ObjDiff {
} }
for (symbol_idx, _) in obj.common.iter().enumerate() { for (symbol_idx, _) in obj.common.iter().enumerate() {
result.common.push(ObjSymbolDiff { result.common.push(ObjSymbolDiff {
symbol_ref: SymbolRef { section_idx: obj.sections.len(), symbol_idx }, symbol_ref: SymbolRef { section_idx: SECTION_COMMON, symbol_idx },
diff_symbol: None, target_symbol: None,
instructions: vec![], instructions: vec![],
match_percent: None, match_percent: None,
}); });
@@ -348,7 +361,7 @@ impl ObjDiff {
#[inline] #[inline]
pub fn symbol_diff(&self, symbol_ref: SymbolRef) -> &ObjSymbolDiff { pub fn symbol_diff(&self, symbol_ref: SymbolRef) -> &ObjSymbolDiff {
if symbol_ref.section_idx == self.sections.len() { if symbol_ref.section_idx == SECTION_COMMON {
&self.common[symbol_ref.symbol_idx] &self.common[symbol_ref.symbol_idx]
} else { } else {
&self.section_diff(symbol_ref.section_idx).symbols[symbol_ref.symbol_idx] &self.section_diff(symbol_ref.section_idx).symbols[symbol_ref.symbol_idx]
@@ -357,7 +370,7 @@ impl ObjDiff {
#[inline] #[inline]
pub fn symbol_diff_mut(&mut self, symbol_ref: SymbolRef) -> &mut ObjSymbolDiff { pub fn symbol_diff_mut(&mut self, symbol_ref: SymbolRef) -> &mut ObjSymbolDiff {
if symbol_ref.section_idx == self.sections.len() { if symbol_ref.section_idx == SECTION_COMMON {
&mut self.common[symbol_ref.symbol_idx] &mut self.common[symbol_ref.symbol_idx]
} else { } else {
&mut self.section_diff_mut(symbol_ref.section_idx).symbols[symbol_ref.symbol_idx] &mut self.section_diff_mut(symbol_ref.section_idx).symbols[symbol_ref.symbol_idx]
@@ -378,7 +391,7 @@ pub fn diff_objs(
right: Option<&ObjInfo>, right: Option<&ObjInfo>,
prev: Option<&ObjInfo>, prev: Option<&ObjInfo>,
) -> Result<DiffObjsResult> { ) -> Result<DiffObjsResult> {
let symbol_matches = matching_symbols(left, right, prev)?; let symbol_matches = matching_symbols(left, right, prev, &config.symbol_mappings)?;
let section_matches = matching_sections(left, right)?; let section_matches = matching_sections(left, right)?;
let mut left = left.map(|p| (p, ObjDiff::new_from_obj(p))); let mut left = left.map(|p| (p, ObjDiff::new_from_obj(p)));
let mut right = right.map(|p| (p, ObjDiff::new_from_obj(p))); let mut right = right.map(|p| (p, ObjDiff::new_from_obj(p)));
@@ -399,6 +412,8 @@ pub fn diff_objs(
let left_code = process_code_symbol(left_obj, left_symbol_ref, config)?; let left_code = process_code_symbol(left_obj, left_symbol_ref, config)?;
let right_code = process_code_symbol(right_obj, right_symbol_ref, config)?; let right_code = process_code_symbol(right_obj, right_symbol_ref, config)?;
let (left_diff, right_diff) = diff_code( let (left_diff, right_diff) = diff_code(
left_obj,
right_obj,
&left_code, &left_code,
&right_code, &right_code,
left_symbol_ref, left_symbol_ref,
@@ -412,6 +427,8 @@ pub fn diff_objs(
let (prev_obj, prev_out) = prev.as_mut().unwrap(); let (prev_obj, prev_out) = prev.as_mut().unwrap();
let prev_code = process_code_symbol(prev_obj, prev_symbol_ref, config)?; let prev_code = process_code_symbol(prev_obj, prev_symbol_ref, config)?;
let (_, prev_diff) = diff_code( let (_, prev_diff) = diff_code(
left_obj,
right_obj,
&right_code, &right_code,
&prev_code, &prev_code,
right_symbol_ref, right_symbol_ref,
@@ -529,6 +546,17 @@ pub fn diff_objs(
} }
} }
if let (Some((right_obj, right_out)), Some((left_obj, left_out))) =
(right.as_mut(), left.as_mut())
{
if let Some(right_name) = &config.symbol_mappings.selecting_left {
generate_mapping_symbols(right_obj, right_name, left_obj, left_out, config)?;
}
if let Some(left_name) = &config.symbol_mappings.selecting_right {
generate_mapping_symbols(left_obj, left_name, right_obj, right_out, config)?;
}
}
Ok(DiffObjsResult { Ok(DiffObjsResult {
left: left.map(|(_, o)| o), left: left.map(|(_, o)| o),
right: right.map(|(_, o)| o), right: right.map(|(_, o)| o),
@@ -536,6 +564,65 @@ pub fn diff_objs(
}) })
} }
/// When we're selecting a symbol to use as a comparison, we'll create comparisons for all
/// symbols in the other object that match the selected symbol's section and kind. This allows
/// us to display match percentages for all symbols in the other object that could be selected.
fn generate_mapping_symbols(
base_obj: &ObjInfo,
base_name: &str,
target_obj: &ObjInfo,
target_out: &mut ObjDiff,
config: &DiffObjConfig,
) -> Result<()> {
let Some(base_symbol_ref) = symbol_ref_by_name(base_obj, base_name) else {
return Ok(());
};
let (base_section, base_symbol) = base_obj.section_symbol(base_symbol_ref);
let Some(base_section) = base_section else {
return Ok(());
};
let base_code = match base_section.kind {
ObjSectionKind::Code => Some(process_code_symbol(base_obj, base_symbol_ref, config)?),
_ => None,
};
for (target_section_index, target_section) in
target_obj.sections.iter().enumerate().filter(|(_, s)| s.kind == base_section.kind)
{
for (target_symbol_index, _target_symbol) in
target_section.symbols.iter().enumerate().filter(|(_, s)| s.kind == base_symbol.kind)
{
let target_symbol_ref =
SymbolRef { section_idx: target_section_index, symbol_idx: target_symbol_index };
match base_section.kind {
ObjSectionKind::Code => {
let target_code = process_code_symbol(target_obj, target_symbol_ref, config)?;
let (left_diff, _right_diff) = diff_code(
target_obj,
base_obj,
&target_code,
base_code.as_ref().unwrap(),
target_symbol_ref,
base_symbol_ref,
config,
)?;
target_out.mapping_symbols.push(left_diff);
}
ObjSectionKind::Data => {
let (left_diff, _right_diff) =
diff_data_symbol(target_obj, base_obj, target_symbol_ref, base_symbol_ref)?;
target_out.mapping_symbols.push(left_diff);
}
ObjSectionKind::Bss => {
let (left_diff, _right_diff) =
diff_bss_symbol(target_obj, base_obj, target_symbol_ref, base_symbol_ref)?;
target_out.mapping_symbols.push(left_diff);
}
}
}
}
Ok(())
}
#[derive(Copy, Clone, Eq, PartialEq)] #[derive(Copy, Clone, Eq, PartialEq)]
struct SymbolMatch { struct SymbolMatch {
left: Option<SymbolRef>, left: Option<SymbolRef>,
@@ -551,19 +638,115 @@ struct SectionMatch {
section_kind: ObjSectionKind, section_kind: ObjSectionKind,
} }
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, serde::Deserialize, serde::Serialize)]
pub struct MappingConfig {
/// Manual symbol mappings
pub mappings: SymbolMappings,
/// The right object symbol name that we're selecting a left symbol for
pub selecting_left: Option<String>,
/// The left object symbol name that we're selecting a right symbol for
pub selecting_right: Option<String>,
}
fn symbol_ref_by_name(obj: &ObjInfo, name: &str) -> Option<SymbolRef> {
for (section_idx, section) in obj.sections.iter().enumerate() {
for (symbol_idx, symbol) in section.symbols.iter().enumerate() {
if symbol.name == name {
return Some(SymbolRef { section_idx, symbol_idx });
}
}
}
None
}
fn apply_symbol_mappings(
left: &ObjInfo,
right: &ObjInfo,
mapping_config: &MappingConfig,
left_used: &mut HashSet<SymbolRef>,
right_used: &mut HashSet<SymbolRef>,
matches: &mut Vec<SymbolMatch>,
) -> Result<()> {
// If we're selecting a symbol to use as a comparison, mark it as used
// This ensures that we don't match it to another symbol at any point
if let Some(left_name) = &mapping_config.selecting_left {
if let Some(left_symbol) = symbol_ref_by_name(left, left_name) {
left_used.insert(left_symbol);
}
}
if let Some(right_name) = &mapping_config.selecting_right {
if let Some(right_symbol) = symbol_ref_by_name(right, right_name) {
right_used.insert(right_symbol);
}
}
// Apply manual symbol mappings
for (left_name, right_name) in &mapping_config.mappings {
let Some(left_symbol) = symbol_ref_by_name(left, left_name) else {
continue;
};
if left_used.contains(&left_symbol) {
continue;
}
let Some(right_symbol) = symbol_ref_by_name(right, right_name) else {
continue;
};
if right_used.contains(&right_symbol) {
continue;
}
let left_section = &left.sections[left_symbol.section_idx];
let right_section = &right.sections[right_symbol.section_idx];
if left_section.kind != right_section.kind {
log::warn!(
"Symbol section kind mismatch: {} ({:?}) vs {} ({:?})",
left_name,
left_section.kind,
right_name,
right_section.kind
);
continue;
}
matches.push(SymbolMatch {
left: Some(left_symbol),
right: Some(right_symbol),
prev: None, // TODO
section_kind: left_section.kind,
});
left_used.insert(left_symbol);
right_used.insert(right_symbol);
}
Ok(())
}
/// Find matching symbols between each object. /// Find matching symbols between each object.
fn matching_symbols( fn matching_symbols(
left: Option<&ObjInfo>, left: Option<&ObjInfo>,
right: Option<&ObjInfo>, right: Option<&ObjInfo>,
prev: Option<&ObjInfo>, prev: Option<&ObjInfo>,
mappings: &MappingConfig,
) -> Result<Vec<SymbolMatch>> { ) -> Result<Vec<SymbolMatch>> {
let mut matches = Vec::new(); let mut matches = Vec::new();
let mut left_used = HashSet::new();
let mut right_used = HashSet::new(); let mut right_used = HashSet::new();
if let Some(left) = left { if let Some(left) = left {
if let Some(right) = right {
apply_symbol_mappings(
left,
right,
mappings,
&mut left_used,
&mut right_used,
&mut matches,
)?;
}
for (section_idx, section) in left.sections.iter().enumerate() { for (section_idx, section) in left.sections.iter().enumerate() {
for (symbol_idx, symbol) in section.symbols.iter().enumerate() { for (symbol_idx, symbol) in section.symbols.iter().enumerate() {
let symbol_ref = SymbolRef { section_idx, symbol_idx };
if left_used.contains(&symbol_ref) {
continue;
}
let symbol_match = SymbolMatch { let symbol_match = SymbolMatch {
left: Some(SymbolRef { section_idx, symbol_idx }), left: Some(symbol_ref),
right: find_symbol(right, symbol, section, Some(&right_used)), right: find_symbol(right, symbol, section, Some(&right_used)),
prev: find_symbol(prev, symbol, section, None), prev: find_symbol(prev, symbol, section, None),
section_kind: section.kind, section_kind: section.kind,
@@ -575,8 +758,12 @@ fn matching_symbols(
} }
} }
for (symbol_idx, symbol) in left.common.iter().enumerate() { for (symbol_idx, symbol) in left.common.iter().enumerate() {
let symbol_ref = SymbolRef { section_idx: SECTION_COMMON, symbol_idx };
if left_used.contains(&symbol_ref) {
continue;
}
let symbol_match = SymbolMatch { let symbol_match = SymbolMatch {
left: Some(SymbolRef { section_idx: left.sections.len(), symbol_idx }), left: Some(symbol_ref),
right: find_common_symbol(right, symbol), right: find_common_symbol(right, symbol),
prev: find_common_symbol(prev, symbol), prev: find_common_symbol(prev, symbol),
section_kind: ObjSectionKind::Bss, section_kind: ObjSectionKind::Bss,
@@ -603,7 +790,7 @@ fn matching_symbols(
} }
} }
for (symbol_idx, symbol) in right.common.iter().enumerate() { for (symbol_idx, symbol) in right.common.iter().enumerate() {
let symbol_ref = SymbolRef { section_idx: right.sections.len(), symbol_idx }; let symbol_ref = SymbolRef { section_idx: SECTION_COMMON, symbol_idx };
if right_used.contains(&symbol_ref) { if right_used.contains(&symbol_ref) {
continue; continue;
} }
@@ -696,7 +883,7 @@ fn find_common_symbol(obj: Option<&ObjInfo>, in_symbol: &ObjSymbol) -> Option<Sy
let obj = obj?; let obj = obj?;
for (symbol_idx, symbol) in obj.common.iter().enumerate() { for (symbol_idx, symbol) in obj.common.iter().enumerate() {
if symbol.name == in_symbol.name { if symbol.name == in_symbol.name {
return Some(SymbolRef { section_idx: obj.sections.len(), symbol_idx }); return Some(SymbolRef { section_idx: SECTION_COMMON, symbol_idx });
} }
} }
None None

View File

@@ -8,4 +8,5 @@ pub mod config;
pub mod diff; pub mod diff;
#[cfg(feature = "any-arch")] #[cfg(feature = "any-arch")]
pub mod obj; pub mod obj;
#[cfg(feature = "any-arch")]
pub mod util; pub mod util;

View File

@@ -112,6 +112,15 @@ pub struct ObjIns {
pub orig: Option<String>, pub orig: Option<String>,
} }
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
pub enum ObjSymbolKind {
#[default]
Unknown,
Function,
Object,
Section,
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct ObjSymbol { pub struct ObjSymbol {
pub name: String, pub name: String,
@@ -120,8 +129,9 @@ pub struct ObjSymbol {
pub section_address: u64, pub section_address: u64,
pub size: u64, pub size: u64,
pub size_known: bool, pub size_known: bool,
pub kind: ObjSymbolKind,
pub flags: ObjSymbolFlagSet, pub flags: ObjSymbolFlagSet,
pub addend: i64, pub orig_section_index: Option<usize>,
/// Original virtual address (from .note.split section) /// Original virtual address (from .note.split section)
pub virtual_address: Option<u64>, pub virtual_address: Option<u64>,
/// Original index in object symbol table /// Original index in object symbol table
@@ -145,7 +155,7 @@ pub struct ObjReloc {
pub flags: RelocationFlags, pub flags: RelocationFlags,
pub address: u64, pub address: u64,
pub target: ObjSymbol, pub target: ObjSymbol,
pub target_section: Option<String>, pub addend: i64,
} }
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
@@ -154,9 +164,11 @@ pub struct SymbolRef {
pub symbol_idx: usize, pub symbol_idx: usize,
} }
pub const SECTION_COMMON: usize = usize::MAX - 1;
impl ObjInfo { impl ObjInfo {
pub fn section_symbol(&self, symbol_ref: SymbolRef) -> (Option<&ObjSection>, &ObjSymbol) { pub fn section_symbol(&self, symbol_ref: SymbolRef) -> (Option<&ObjSection>, &ObjSymbol) {
if symbol_ref.section_idx == self.sections.len() { if symbol_ref.section_idx == SECTION_COMMON {
let symbol = &self.common[symbol_ref.symbol_idx]; let symbol = &self.common[symbol_ref.symbol_idx];
return (None, symbol); return (None, symbol);
} }

View File

@@ -13,8 +13,8 @@ use object::{
endian::LittleEndian as LE, endian::LittleEndian as LE,
pe::{ImageAuxSymbolFunctionBeginEnd, ImageLinenumber}, pe::{ImageAuxSymbolFunctionBeginEnd, ImageLinenumber},
read::coff::{CoffFile, CoffHeader, ImageSymbol}, read::coff::{CoffFile, CoffHeader, ImageSymbol},
BinaryFormat, File, Object, ObjectSection, ObjectSymbol, RelocationTarget, SectionIndex, BinaryFormat, File, Object, ObjectSection, ObjectSymbol, RelocationTarget, Section,
SectionKind, Symbol, SymbolIndex, SymbolKind, SymbolScope, SymbolSection, SectionIndex, SectionKind, Symbol, SymbolIndex, SymbolKind, SymbolScope,
}; };
use crate::{ use crate::{
@@ -23,6 +23,7 @@ use crate::{
obj::{ obj::{
split_meta::{SplitMeta, SPLITMETA_SECTION}, split_meta::{SplitMeta, SPLITMETA_SECTION},
ObjInfo, ObjReloc, ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlagSet, ObjSymbolFlags, ObjInfo, ObjReloc, ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlagSet, ObjSymbolFlags,
ObjSymbolKind,
}, },
util::{read_u16, read_u32}, util::{read_u16, read_u32},
}; };
@@ -40,7 +41,6 @@ fn to_obj_symbol(
arch: &dyn ObjArch, arch: &dyn ObjArch,
obj_file: &File<'_>, obj_file: &File<'_>,
symbol: &Symbol<'_, '_>, symbol: &Symbol<'_, '_>,
addend: i64,
split_meta: Option<&SplitMeta>, split_meta: Option<&SplitMeta>,
) -> Result<ObjSymbol> { ) -> Result<ObjSymbol> {
let mut name = symbol.name().context("Failed to process symbol name")?; let mut name = symbol.name().context("Failed to process symbol name")?;
@@ -64,6 +64,7 @@ fn to_obj_symbol(
if obj_file.format() == BinaryFormat::Elf && symbol.scope() == SymbolScope::Linkage { if obj_file.format() == BinaryFormat::Elf && symbol.scope() == SymbolScope::Linkage {
flags = ObjSymbolFlagSet(flags.0 | ObjSymbolFlags::Hidden); flags = ObjSymbolFlagSet(flags.0 | ObjSymbolFlags::Hidden);
} }
#[cfg(feature = "ppc")]
if arch if arch
.ppc() .ppc()
.and_then(|a| a.extab.as_ref()) .and_then(|a| a.extab.as_ref())
@@ -94,6 +95,13 @@ fn to_obj_symbol(
}) })
.unwrap_or(&[]); .unwrap_or(&[]);
let kind = match symbol.kind() {
SymbolKind::Text => ObjSymbolKind::Function,
SymbolKind::Data => ObjSymbolKind::Object,
SymbolKind::Section => ObjSymbolKind::Section,
_ => ObjSymbolKind::Unknown,
};
Ok(ObjSymbol { Ok(ObjSymbol {
name: name.to_string(), name: name.to_string(),
demangled_name, demangled_name,
@@ -101,8 +109,9 @@ fn to_obj_symbol(
section_address, section_address,
size: symbol.size(), size: symbol.size(),
size_known: symbol.size() != 0, size_known: symbol.size() != 0,
kind,
flags, flags,
addend, orig_section_index: symbol.section_index().map(|i| i.0),
virtual_address, virtual_address,
original_index: Some(symbol.index().0), original_index: Some(symbol.index().0),
bytes: bytes.to_vec(), bytes: bytes.to_vec(),
@@ -152,26 +161,23 @@ fn symbols_by_section(
arch: &dyn ObjArch, arch: &dyn ObjArch,
obj_file: &File<'_>, obj_file: &File<'_>,
section: &ObjSection, section: &ObjSection,
section_symbols: &[Symbol<'_, '_>],
split_meta: Option<&SplitMeta>, split_meta: Option<&SplitMeta>,
name_counts: &mut HashMap<String, u32>, name_counts: &mut HashMap<String, u32>,
) -> Result<Vec<ObjSymbol>> { ) -> Result<Vec<ObjSymbol>> {
let mut result = Vec::<ObjSymbol>::new(); let mut result = Vec::<ObjSymbol>::new();
for symbol in obj_file.symbols() { for symbol in section_symbols {
if symbol.kind() == SymbolKind::Section { if symbol.kind() == SymbolKind::Section {
continue; continue;
} }
if let Some(index) = symbol.section().index() { if symbol.is_local() && section.kind == ObjSectionKind::Code {
if index.0 == section.orig_index { // TODO strip local syms in diff?
if symbol.is_local() && section.kind == ObjSectionKind::Code { let name = symbol.name().context("Failed to process symbol name")?;
// TODO strip local syms in diff? if symbol.size() == 0 || name.starts_with("lbl_") {
let name = symbol.name().context("Failed to process symbol name")?; continue;
if symbol.size() == 0 || name.starts_with("lbl_") {
continue;
}
}
result.push(to_obj_symbol(arch, obj_file, &symbol, 0, split_meta)?);
} }
} }
result.push(to_obj_symbol(arch, obj_file, symbol, split_meta)?);
} }
result.sort_by(|a, b| a.address.cmp(&b.address).then(a.size.cmp(&b.size))); result.sort_by(|a, b| a.address.cmp(&b.address).then(a.size.cmp(&b.size)));
let mut iter = result.iter_mut().peekable(); let mut iter = result.iter_mut().peekable();
@@ -182,6 +188,13 @@ fn symbols_by_section(
} else { } else {
symbol.size = (section.address + section.size) - symbol.address; symbol.size = (section.address + section.size) - symbol.address;
} }
// Set symbol kind if we ended up with a non-zero size
if symbol.kind == ObjSymbolKind::Unknown && symbol.size > 0 {
symbol.kind = match section.kind {
ObjSectionKind::Code => ObjSymbolKind::Function,
ObjSectionKind::Data | ObjSectionKind::Bss => ObjSymbolKind::Object,
};
}
} }
} }
if result.is_empty() { if result.is_empty() {
@@ -199,8 +212,12 @@ fn symbols_by_section(
section_address: 0, section_address: 0,
size: section.size, size: section.size,
size_known: true, size_known: true,
kind: match section.kind {
ObjSectionKind::Code => ObjSymbolKind::Function,
ObjSectionKind::Data | ObjSectionKind::Bss => ObjSymbolKind::Object,
},
flags: Default::default(), flags: Default::default(),
addend: 0, orig_section_index: Some(section.orig_index),
virtual_address: None, virtual_address: None,
original_index: None, original_index: None,
bytes: Vec::new(), bytes: Vec::new(),
@@ -217,51 +234,75 @@ fn common_symbols(
obj_file obj_file
.symbols() .symbols()
.filter(Symbol::is_common) .filter(Symbol::is_common)
.map(|symbol| to_obj_symbol(arch, obj_file, &symbol, 0, split_meta)) .map(|symbol| to_obj_symbol(arch, obj_file, &symbol, split_meta))
.collect::<Result<Vec<ObjSymbol>>>() .collect::<Result<Vec<ObjSymbol>>>()
} }
const LOW_PRIORITY_SYMBOLS: &[&str] =
&["__gnu_compiled_c", "__gnu_compiled_cplusplus", "gcc2_compiled."];
fn best_symbol<'r, 'data, 'file>(
symbols: &'r [Symbol<'data, 'file>],
address: u64,
) -> Option<&'r Symbol<'data, 'file>> {
let mut closest_symbol_index = match symbols.binary_search_by_key(&address, |s| s.address()) {
Ok(index) => Some(index),
Err(index) => index.checked_sub(1),
}?;
// The binary search may not find the first symbol at the address, so work backwards
let target_address = symbols[closest_symbol_index].address();
while let Some(prev_index) = closest_symbol_index.checked_sub(1) {
if symbols[prev_index].address() != target_address {
break;
}
closest_symbol_index = prev_index;
}
let mut best_symbol: Option<&'r Symbol<'data, 'file>> = None;
for symbol in symbols.iter().skip(closest_symbol_index) {
if symbol.address() > address {
break;
}
if symbol.kind() == SymbolKind::Section
|| (symbol.size() > 0 && (symbol.address() + symbol.size()) <= address)
{
continue;
}
// TODO priority ranking with visibility, etc
if let Some(best) = best_symbol {
if LOW_PRIORITY_SYMBOLS.contains(&best.name().unwrap_or_default())
&& !LOW_PRIORITY_SYMBOLS.contains(&symbol.name().unwrap_or_default())
{
best_symbol = Some(symbol);
}
} else {
best_symbol = Some(symbol);
}
}
best_symbol
}
fn find_section_symbol( fn find_section_symbol(
arch: &dyn ObjArch, arch: &dyn ObjArch,
obj_file: &File<'_>, obj_file: &File<'_>,
target: &Symbol<'_, '_>, section: &Section,
section_symbols: &[Symbol<'_, '_>],
address: u64, address: u64,
split_meta: Option<&SplitMeta>, split_meta: Option<&SplitMeta>,
) -> Result<ObjSymbol> { ) -> Result<ObjSymbol> {
let section_index = if let Some(symbol) = best_symbol(section_symbols, address) {
target.section_index().ok_or_else(|| anyhow::Error::msg("Unknown section index"))?; return to_obj_symbol(arch, obj_file, symbol, split_meta);
let section = obj_file.section_by_index(section_index)?;
let mut closest_symbol: Option<Symbol<'_, '_>> = None;
for symbol in obj_file.symbols() {
if !matches!(symbol.section_index(), Some(idx) if idx == section_index) {
continue;
}
if symbol.kind() == SymbolKind::Section || symbol.address() != address {
if symbol.address() < address
&& symbol.size() != 0
&& (closest_symbol.is_none()
|| matches!(&closest_symbol, Some(s) if s.address() <= symbol.address()))
{
closest_symbol = Some(symbol);
}
continue;
}
return to_obj_symbol(arch, obj_file, &symbol, 0, split_meta);
} }
let (name, offset) = closest_symbol // Fallback to section symbol
.and_then(|s| s.name().map(|n| (n, s.address())).ok())
.or_else(|| section.name().map(|n| (n, section.address())).ok())
.unwrap_or(("<unknown>", 0));
let offset_addr = address - offset;
Ok(ObjSymbol { Ok(ObjSymbol {
name: name.to_string(), name: section.name()?.to_string(),
demangled_name: None, demangled_name: None,
address: offset, address: section.address(),
section_address: address - section.address(), section_address: 0,
size: 0, size: 0,
size_known: false, size_known: false,
kind: ObjSymbolKind::Section,
flags: Default::default(), flags: Default::default(),
addend: offset_addr as i64, orig_section_index: Some(section.index().0),
virtual_address: None, virtual_address: None,
original_index: None, original_index: None,
bytes: Vec::new(), bytes: Vec::new(),
@@ -272,6 +313,7 @@ fn relocations_by_section(
arch: &dyn ObjArch, arch: &dyn ObjArch,
obj_file: &File<'_>, obj_file: &File<'_>,
section: &ObjSection, section: &ObjSection,
section_symbols: &[Vec<Symbol<'_, '_>>],
split_meta: Option<&SplitMeta>, split_meta: Option<&SplitMeta>,
) -> Result<Vec<ObjReloc>> { ) -> Result<Vec<ObjReloc>> {
let obj_section = obj_file.section_by_index(SectionIndex(section.orig_index))?; let obj_section = obj_file.section_by_index(SectionIndex(section.orig_index))?;
@@ -296,30 +338,36 @@ fn relocations_by_section(
_ => bail!("Unhandled relocation target: {:?}", reloc.target()), _ => bail!("Unhandled relocation target: {:?}", reloc.target()),
}; };
let flags = reloc.flags(); // TODO validate reloc here? let flags = reloc.flags(); // TODO validate reloc here?
let target_section = match symbol.section() { let mut addend = if reloc.has_implicit_addend() {
SymbolSection::Common => Some(".comm".to_string()),
SymbolSection::Section(idx) => {
obj_file.section_by_index(idx).and_then(|s| s.name().map(|s| s.to_string())).ok()
}
_ => None,
};
let addend = if reloc.has_implicit_addend() {
arch.implcit_addend(obj_file, section, address, &reloc)? arch.implcit_addend(obj_file, section, address, &reloc)?
} else { } else {
reloc.addend() reloc.addend()
}; };
// println!("Reloc: {reloc:?}, symbol: {symbol:?}, addend: {addend:#x}");
let target = match symbol.kind() { let target = match symbol.kind() {
SymbolKind::Text | SymbolKind::Data | SymbolKind::Label | SymbolKind::Unknown => { SymbolKind::Text | SymbolKind::Data | SymbolKind::Label | SymbolKind::Unknown => {
to_obj_symbol(arch, obj_file, &symbol, addend, split_meta) to_obj_symbol(arch, obj_file, &symbol, split_meta)?
} }
SymbolKind::Section => { SymbolKind::Section => {
ensure!(addend >= 0, "Negative addend in reloc: {addend}"); ensure!(addend >= 0, "Negative addend in section reloc: {addend}");
find_section_symbol(arch, obj_file, &symbol, addend as u64, split_meta) let section_index = symbol
.section_index()
.ok_or_else(|| anyhow!("Section symbol {symbol:?} has no section index"))?;
let section = obj_file.section_by_index(section_index)?;
let symbol = find_section_symbol(
arch,
obj_file,
&section,
&section_symbols[section_index.0],
addend as u64,
split_meta,
)?;
// Adjust addend to be relative to the selected symbol
addend = (symbol.address - section.address()) as i64;
symbol
} }
kind => Err(anyhow!("Unhandled relocation symbol type {kind:?}")), kind => bail!("Unhandled relocation symbol type {kind:?}"),
}?; };
relocations.push(ObjReloc { flags, address, target, target_section }); relocations.push(ObjReloc { flags, address, target, addend });
} }
Ok(relocations) Ok(relocations)
} }
@@ -384,9 +432,9 @@ fn line_info(obj_file: &File<'_>, sections: &mut [ObjSection], obj_data: &[u8])
let mut text_sections = let mut text_sections =
obj_file.sections().filter(|s| s.kind() == SectionKind::Text); obj_file.sections().filter(|s| s.kind() == SectionKind::Text);
let section_index = text_sections.next().map(|s| s.index().0); let section_index = text_sections.next().map(|s| s.index().0);
let mut lines = section_index.map(|index| { let mut lines = section_index
&mut sections.iter_mut().find(|s| s.orig_index == index).unwrap().line_info .and_then(|index| sections.iter_mut().find(|s| s.orig_index == index))
}); .map(|s| &mut s.line_info);
let mut rows = program.rows(); let mut rows = program.rows();
while let Some((_header, row)) = rows.next_row()? { while let Some((_header, row)) = rows.next_row()? {
@@ -397,13 +445,9 @@ fn line_info(obj_file: &File<'_>, sections: &mut [ObjSection], obj_data: &[u8])
// The next row is the start of a new sequence, which means we must // The next row is the start of a new sequence, which means we must
// advance to the next .text section. // advance to the next .text section.
let section_index = text_sections.next().map(|s| s.index().0); let section_index = text_sections.next().map(|s| s.index().0);
lines = section_index.map(|index| { lines = section_index
&mut sections .and_then(|index| sections.iter_mut().find(|s| s.orig_index == index))
.iter_mut() .map(|s| &mut s.line_info);
.find(|s| s.orig_index == index)
.unwrap()
.line_info
});
} }
} }
} }
@@ -539,8 +583,9 @@ fn update_combined_symbol(symbol: ObjSymbol, address_change: i64) -> Result<ObjS
section_address: (symbol.section_address as i64 + address_change).try_into()?, section_address: (symbol.section_address as i64 + address_change).try_into()?,
size: symbol.size, size: symbol.size,
size_known: symbol.size_known, size_known: symbol.size_known,
kind: symbol.kind,
flags: symbol.flags, flags: symbol.flags,
addend: symbol.addend, orig_section_index: symbol.orig_section_index,
virtual_address: if let Some(virtual_address) = symbol.virtual_address { virtual_address: if let Some(virtual_address) = symbol.virtual_address {
Some((virtual_address as i64 + address_change).try_into()?) Some((virtual_address as i64 + address_change).try_into()?)
} else { } else {
@@ -566,8 +611,8 @@ fn combine_sections(section: ObjSection, combine: ObjSection) -> Result<ObjSecti
relocations.push(ObjReloc { relocations.push(ObjReloc {
flags: reloc.flags, flags: reloc.flags,
address: (reloc.address as i64 + address_change).try_into()?, address: (reloc.address as i64 + address_change).try_into()?,
target: reloc.target, // TODO: Should be updated? target: reloc.target, // TODO: Should be updated?
target_section: reloc.target_section, // TODO: Same as above addend: reloc.addend,
}); });
} }
@@ -647,18 +692,40 @@ pub fn parse(data: &[u8], config: &DiffObjConfig) -> Result<ObjInfo> {
let obj_file = File::parse(data)?; let obj_file = File::parse(data)?;
let arch = new_arch(&obj_file)?; let arch = new_arch(&obj_file)?;
let split_meta = split_meta(&obj_file)?; let split_meta = split_meta(&obj_file)?;
// Create sorted symbol list for each section
let mut section_symbols = Vec::with_capacity(obj_file.sections().count());
for section in obj_file.sections() {
let mut symbols = obj_file
.symbols()
.filter(|s| s.section_index() == Some(section.index()))
.collect::<Vec<_>>();
symbols.sort_by_key(|s| s.address());
let section_index = section.index().0;
if section_index >= section_symbols.len() {
section_symbols.resize_with(section_index + 1, Vec::new);
}
section_symbols[section_index] = symbols;
}
let mut sections = filter_sections(&obj_file, split_meta.as_ref())?; let mut sections = filter_sections(&obj_file, split_meta.as_ref())?;
let mut name_counts: HashMap<String, u32> = HashMap::new(); let mut section_name_counts: HashMap<String, u32> = HashMap::new();
for section in &mut sections { for section in &mut sections {
section.symbols = symbols_by_section( section.symbols = symbols_by_section(
arch.as_ref(), arch.as_ref(),
&obj_file, &obj_file,
section, section,
&section_symbols[section.orig_index],
split_meta.as_ref(),
&mut section_name_counts,
)?;
section.relocations = relocations_by_section(
arch.as_ref(),
&obj_file,
section,
&section_symbols,
split_meta.as_ref(), split_meta.as_ref(),
&mut name_counts,
)?; )?;
section.relocations =
relocations_by_section(arch.as_ref(), &obj_file, section, split_meta.as_ref())?;
} }
if config.combine_data_sections { if config.combine_data_sections {
combine_data_sections(&mut sections)?; combine_data_sections(&mut sections)?;

View File

@@ -15,7 +15,8 @@ use globset::{Glob, GlobSet};
use notify::{RecursiveMode, Watcher}; use notify::{RecursiveMode, Watcher};
use objdiff_core::{ use objdiff_core::{
config::{ config::{
build_globset, ProjectConfigInfo, ProjectObject, ScratchConfig, DEFAULT_WATCH_PATTERNS, build_globset, save_project_config, ProjectConfig, ProjectConfigInfo, ProjectObject,
ScratchConfig, SymbolMappings, DEFAULT_WATCH_PATTERNS,
}, },
diff::DiffObjConfig, diff::DiffObjConfig,
}; };
@@ -42,11 +43,10 @@ use crate::{
graphics::{graphics_window, GraphicsConfig, GraphicsViewState}, graphics::{graphics_window, GraphicsConfig, GraphicsViewState},
jobs::{jobs_menu_ui, jobs_window}, jobs::{jobs_menu_ui, jobs_window},
rlwinm::{rlwinm_decode_window, RlwinmDecodeViewState}, rlwinm::{rlwinm_decode_window, RlwinmDecodeViewState},
symbol_diff::{symbol_diff_ui, DiffViewState, View}, symbol_diff::{symbol_diff_ui, DiffViewAction, DiffViewNavigation, DiffViewState, View},
}, },
}; };
#[derive(Default)]
pub struct ViewState { pub struct ViewState {
pub jobs: JobQueue, pub jobs: JobQueue,
pub config_state: ConfigViewState, pub config_state: ConfigViewState,
@@ -63,10 +63,34 @@ pub struct ViewState {
pub show_debug: bool, pub show_debug: bool,
pub show_graphics: bool, pub show_graphics: bool,
pub show_jobs: bool, pub show_jobs: bool,
pub show_side_panel: bool,
}
impl Default for ViewState {
fn default() -> Self {
Self {
jobs: Default::default(),
config_state: Default::default(),
demangle_state: Default::default(),
rlwinm_decode_state: Default::default(),
diff_state: Default::default(),
graphics_state: Default::default(),
frame_history: Default::default(),
show_appearance_config: false,
show_demangle: false,
show_rlwinm_decode: false,
show_project_config: false,
show_arch_config: false,
show_debug: false,
show_graphics: false,
show_jobs: false,
show_side_panel: true,
}
}
} }
/// The configuration for a single object file. /// The configuration for a single object file.
#[derive(Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)] #[derive(Default, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct ObjectConfig { pub struct ObjectConfig {
pub name: String, pub name: String,
pub target_path: Option<PathBuf>, pub target_path: Option<PathBuf>,
@@ -75,6 +99,23 @@ pub struct ObjectConfig {
pub complete: Option<bool>, pub complete: Option<bool>,
pub scratch: Option<ScratchConfig>, pub scratch: Option<ScratchConfig>,
pub source_path: Option<String>, pub source_path: Option<String>,
#[serde(default)]
pub symbol_mappings: SymbolMappings,
}
impl From<&ProjectObject> for ObjectConfig {
fn from(object: &ProjectObject) -> Self {
Self {
name: object.name().to_string(),
target_path: object.target_path.clone(),
base_path: object.base_path.clone(),
reverse_fn_order: object.reverse_fn_order(),
complete: object.complete(),
scratch: object.scratch.clone(),
source_path: object.source_path().cloned(),
symbol_mappings: object.symbol_mappings.clone().unwrap_or_default(),
}
}
} }
#[inline] #[inline]
@@ -94,8 +135,14 @@ pub struct AppState {
pub obj_change: bool, pub obj_change: bool,
pub queue_build: bool, pub queue_build: bool,
pub queue_reload: bool, pub queue_reload: bool,
pub current_project_config: Option<ProjectConfig>,
pub project_config_info: Option<ProjectConfigInfo>, pub project_config_info: Option<ProjectConfigInfo>,
pub last_mod_check: Instant, pub last_mod_check: Instant,
/// The right object symbol name that we're selecting a left symbol for
pub selecting_left: Option<String>,
/// The left object symbol name that we're selecting a right symbol for
pub selecting_right: Option<String>,
pub config_error: Option<String>,
} }
impl Default for AppState { impl Default for AppState {
@@ -109,8 +156,12 @@ impl Default for AppState {
obj_change: false, obj_change: false,
queue_build: false, queue_build: false,
queue_reload: false, queue_reload: false,
current_project_config: None,
project_config_info: None, project_config_info: None,
last_mod_check: Instant::now(), last_mod_check: Instant::now(),
selecting_left: None,
selecting_right: None,
config_error: None,
} }
} }
} }
@@ -191,7 +242,10 @@ impl AppState {
self.config_change = true; self.config_change = true;
self.obj_change = true; self.obj_change = true;
self.queue_build = false; self.queue_build = false;
self.current_project_config = None;
self.project_config_info = None; self.project_config_info = None;
self.selecting_left = None;
self.selecting_right = None;
} }
pub fn set_target_obj_dir(&mut self, path: PathBuf) { pub fn set_target_obj_dir(&mut self, path: PathBuf) {
@@ -199,6 +253,8 @@ impl AppState {
self.config.selected_obj = None; self.config.selected_obj = None;
self.obj_change = true; self.obj_change = true;
self.queue_build = false; self.queue_build = false;
self.selecting_left = None;
self.selecting_right = None;
} }
pub fn set_base_obj_dir(&mut self, path: PathBuf) { pub fn set_base_obj_dir(&mut self, path: PathBuf) {
@@ -206,12 +262,132 @@ impl AppState {
self.config.selected_obj = None; self.config.selected_obj = None;
self.obj_change = true; self.obj_change = true;
self.queue_build = false; self.queue_build = false;
self.selecting_left = None;
self.selecting_right = None;
} }
pub fn set_selected_obj(&mut self, object: ObjectConfig) { pub fn set_selected_obj(&mut self, config: ObjectConfig) {
self.config.selected_obj = Some(object); let mut unit_changed = true;
if let Some(existing) = self.config.selected_obj.as_ref() {
if existing == &config {
// Don't reload the object if there were no changes
return;
}
if existing.name == config.name {
unit_changed = false;
}
}
self.config.selected_obj = Some(config);
if unit_changed {
self.obj_change = true;
self.queue_build = false;
self.selecting_left = None;
self.selecting_right = None;
} else {
self.queue_build = true;
}
}
pub fn clear_selected_obj(&mut self) {
self.config.selected_obj = None;
self.obj_change = true; self.obj_change = true;
self.queue_build = false; self.queue_build = false;
self.selecting_left = None;
self.selecting_right = None;
}
pub fn set_selecting_left(&mut self, right: &str) {
let Some(object) = self.config.selected_obj.as_mut() else {
return;
};
object.symbol_mappings.remove_by_right(right);
self.selecting_left = Some(right.to_string());
self.queue_reload = true;
self.save_config();
}
pub fn set_selecting_right(&mut self, left: &str) {
let Some(object) = self.config.selected_obj.as_mut() else {
return;
};
object.symbol_mappings.remove_by_left(left);
self.selecting_right = Some(left.to_string());
self.queue_reload = true;
self.save_config();
}
pub fn set_symbol_mapping(&mut self, left: String, right: String) {
let Some(object) = self.config.selected_obj.as_mut() else {
log::warn!("No selected object");
return;
};
self.selecting_left = None;
self.selecting_right = None;
if left == right {
object.symbol_mappings.remove_by_left(&left);
object.symbol_mappings.remove_by_right(&right);
} else {
object.symbol_mappings.insert(left.clone(), right.clone());
}
self.queue_reload = true;
self.save_config();
}
pub fn clear_selection(&mut self) {
self.selecting_left = None;
self.selecting_right = None;
self.queue_reload = true;
}
pub fn clear_mappings(&mut self) {
self.selecting_left = None;
self.selecting_right = None;
if let Some(object) = self.config.selected_obj.as_mut() {
object.symbol_mappings.clear();
}
self.queue_reload = true;
self.save_config();
}
pub fn is_selecting_symbol(&self) -> bool {
self.selecting_left.is_some() || self.selecting_right.is_some()
}
pub fn save_config(&mut self) {
let (Some(config), Some(info)) =
(self.current_project_config.as_mut(), self.project_config_info.as_mut())
else {
return;
};
// Update the project config with the current state
if let Some(object) = self.config.selected_obj.as_ref() {
if let Some(existing) = config.units.as_mut().and_then(|v| {
v.iter_mut().find(|u| u.name.as_ref().is_some_and(|n| n == &object.name))
}) {
existing.symbol_mappings = if object.symbol_mappings.is_empty() {
None
} else {
Some(object.symbol_mappings.clone())
};
}
if let Some(existing) =
self.objects.iter_mut().find(|u| u.name.as_ref().is_some_and(|n| n == &object.name))
{
existing.symbol_mappings = if object.symbol_mappings.is_empty() {
None
} else {
Some(object.symbol_mappings.clone())
};
}
}
// Save the updated project config
match save_project_config(config, info) {
Ok(new_info) => *info = new_info,
Err(e) => {
log::error!("Failed to save project config: {e}");
self.config_error = Some(format!("Failed to save project config: {e}"));
}
}
} }
} }
@@ -350,31 +526,41 @@ impl App {
debug_assert!(jobs.results.is_empty()); debug_assert!(jobs.results.is_empty());
} }
fn post_update(&mut self, ctx: &egui::Context) { fn post_update(&mut self, ctx: &egui::Context, action: Option<DiffViewAction>) {
self.appearance.post_update(ctx); self.appearance.post_update(ctx);
let ViewState { jobs, diff_state, config_state, graphics_state, .. } = &mut self.view_state; let ViewState { jobs, diff_state, config_state, graphics_state, .. } = &mut self.view_state;
config_state.post_update(ctx, jobs, &self.state); config_state.post_update(ctx, jobs, &self.state);
diff_state.post_update(ctx, jobs, &self.state); diff_state.post_update(action, ctx, jobs, &self.state);
let Ok(mut state) = self.state.write() else { let Ok(mut state) = self.state.write() else {
return; return;
}; };
let state = &mut *state; let state = &mut *state;
if let Some(info) = &state.project_config_info { let mut mod_check = false;
if file_modified(&info.path, info.timestamp) { if state.last_mod_check.elapsed().as_millis() >= 500 {
state.config_change = true; state.last_mod_check = Instant::now();
mod_check = true;
}
if mod_check {
if let Some(info) = &state.project_config_info {
if let Some(last_ts) = info.timestamp {
if file_modified(&info.path, last_ts) {
state.config_change = true;
}
}
} }
} }
if state.config_change { if state.config_change {
state.config_change = false; state.config_change = false;
match load_project_config(state) { match load_project_config(state) {
Ok(()) => config_state.load_error = None, Ok(()) => state.config_error = None,
Err(e) => { Err(e) => {
log::error!("Failed to load project config: {e}"); log::error!("Failed to load project config: {e}");
config_state.load_error = Some(format!("{e}")); state.config_error = Some(format!("{e}"));
} }
} }
} }
@@ -409,8 +595,7 @@ impl App {
} }
if let Some(result) = &diff_state.build { if let Some(result) = &diff_state.build {
if state.last_mod_check.elapsed().as_millis() >= 500 { if mod_check {
state.last_mod_check = Instant::now();
if let Some((obj, _)) = &result.first_obj { if let Some((obj, _)) = &result.first_obj {
if let (Some(path), Some(timestamp)) = (&obj.path, obj.timestamp) { if let (Some(path), Some(timestamp)) = (&obj.path, obj.timestamp) {
if file_modified(path, timestamp) { if file_modified(path, timestamp) {
@@ -434,11 +619,11 @@ impl App {
&& state.config.selected_obj.is_some() && state.config.selected_obj.is_some()
&& !jobs.is_running(Job::ObjDiff) && !jobs.is_running(Job::ObjDiff)
{ {
jobs.push(start_build(ctx, ObjDiffConfig::from_config(&state.config))); jobs.push(start_build(ctx, ObjDiffConfig::from_state(state)));
state.queue_build = false; state.queue_build = false;
state.queue_reload = false; state.queue_reload = false;
} else if state.queue_reload && !jobs.is_running(Job::ObjDiff) { } else if state.queue_reload && !jobs.is_running(Job::ObjDiff) {
let mut diff_config = ObjDiffConfig::from_config(&state.config); let mut diff_config = ObjDiffConfig::from_state(state);
// Don't build, just reload the current files // Don't build, just reload the current files
diff_config.build_base = false; diff_config.build_base = false;
diff_config.build_target = false; diff_config.build_target = false;
@@ -485,12 +670,26 @@ impl eframe::App for App {
show_debug, show_debug,
show_graphics, show_graphics,
show_jobs, show_jobs,
show_side_panel,
} = view_state; } = view_state;
frame_history.on_new_frame(ctx.input(|i| i.time), frame.info().cpu_usage); frame_history.on_new_frame(ctx.input(|i| i.time), frame.info().cpu_usage);
let side_panel_available = diff_state.current_view == View::SymbolDiff;
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| { egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
egui::menu::bar(ui, |ui| { egui::menu::bar(ui, |ui| {
if ui
.add_enabled(
side_panel_available,
egui::Button::new(if *show_side_panel { "" } else { "" }),
)
.on_hover_text("Toggle side panel")
.clicked()
{
*show_side_panel = !*show_side_panel;
}
ui.separator();
ui.menu_button("File", |ui| { ui.menu_button("File", |ui| {
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
if ui.button("Debug…").clicked() { if ui.button("Debug…").clicked() {
@@ -599,6 +798,11 @@ impl eframe::App for App {
{ {
state.queue_reload = true; state.queue_reload = true;
} }
if ui.button("Clear custom symbol mappings").clicked() {
state.clear_mappings();
diff_state.post_build_nav = Some(DiffViewNavigation::symbol_diff());
state.queue_reload = true;
}
}); });
ui.separator(); ui.separator();
if jobs_menu_ui(ui, jobs, appearance) { if jobs_menu_ui(ui, jobs, appearance) {
@@ -607,31 +811,28 @@ impl eframe::App for App {
}); });
}); });
let build_success = matches!(&diff_state.build, Some(b) if b.first_status.success && b.second_status.success); if side_panel_available {
if diff_state.current_view == View::FunctionDiff && build_success { egui::SidePanel::left("side_panel").show_animated(ctx, *show_side_panel, |ui| {
egui::CentralPanel::default().show(ctx, |ui| {
function_diff_ui(ui, diff_state, appearance);
});
} else if diff_state.current_view == View::DataDiff && build_success {
egui::CentralPanel::default().show(ctx, |ui| {
data_diff_ui(ui, diff_state, appearance);
});
} else if diff_state.current_view == View::ExtabDiff && build_success {
egui::CentralPanel::default().show(ctx, |ui| {
extab_diff_ui(ui, diff_state, appearance);
});
} else {
egui::SidePanel::left("side_panel").show(ctx, |ui| {
egui::ScrollArea::both().show(ui, |ui| { egui::ScrollArea::both().show(ui, |ui| {
config_ui(ui, state, show_project_config, config_state, appearance); config_ui(ui, state, show_project_config, config_state, appearance);
}); });
}); });
egui::CentralPanel::default().show(ctx, |ui| {
symbol_diff_ui(ui, diff_state, appearance);
});
} }
let mut action = None;
egui::CentralPanel::default().show(ctx, |ui| {
let build_success = matches!(&diff_state.build, Some(b) if b.first_status.success && b.second_status.success);
action = if diff_state.current_view == View::FunctionDiff && build_success {
function_diff_ui(ui, diff_state, appearance)
} else if diff_state.current_view == View::DataDiff && build_success {
data_diff_ui(ui, diff_state, appearance)
} else if diff_state.current_view == View::ExtabDiff && build_success {
extab_diff_ui(ui, diff_state, appearance)
} else {
symbol_diff_ui(ui, diff_state, appearance)
};
});
project_window(ctx, state, show_project_config, config_state, appearance); project_window(ctx, state, show_project_config, config_state, appearance);
appearance_window(ctx, show_appearance_config, appearance); appearance_window(ctx, show_appearance_config, appearance);
demangle_window(ctx, show_demangle, demangle_state, appearance); demangle_window(ctx, show_demangle, demangle_state, appearance);
@@ -641,10 +842,10 @@ impl eframe::App for App {
graphics_window(ctx, show_graphics, frame_history, graphics_state, appearance); graphics_window(ctx, show_graphics, frame_history, graphics_state, appearance);
jobs_window(ctx, show_jobs, jobs, appearance); jobs_window(ctx, show_jobs, jobs, appearance);
self.post_update(ctx); self.post_update(ctx, action);
} }
/// Called by the frame work to save state before shutdown. /// Called by the framework to save state before shutdown.
fn save(&mut self, storage: &mut dyn eframe::Storage) { fn save(&mut self, storage: &mut dyn eframe::Storage) {
if let Ok(state) = self.state.read() { if let Ok(state) = self.state.read() {
eframe::set_value(storage, CONFIG_KEY, &state.config); eframe::set_value(storage, CONFIG_KEY, &state.config);

View File

@@ -2,6 +2,10 @@ use std::path::PathBuf;
use eframe::Storage; use eframe::Storage;
use globset::Glob; use globset::Glob;
use objdiff_core::{
config::ScratchConfig,
diff::{ArmArchVersion, ArmR9Usage, DiffObjConfig, MipsAbi, MipsInstrCategory, X86Formatter},
};
use crate::app::{AppConfig, ObjectConfig, CONFIG_KEY}; use crate::app::{AppConfig, ObjectConfig, CONFIG_KEY};
@@ -11,7 +15,7 @@ pub struct AppConfigVersion {
} }
impl Default for AppConfigVersion { impl Default for AppConfigVersion {
fn default() -> Self { Self { version: 1 } } fn default() -> Self { Self { version: 2 } }
} }
/// Deserialize the AppConfig from storage, handling upgrades from older versions. /// Deserialize the AppConfig from storage, handling upgrades from older versions.
@@ -19,7 +23,8 @@ pub fn deserialize_config(storage: &dyn Storage) -> Option<AppConfig> {
let str = storage.get_string(CONFIG_KEY)?; let str = storage.get_string(CONFIG_KEY)?;
match ron::from_str::<AppConfigVersion>(&str) { match ron::from_str::<AppConfigVersion>(&str) {
Ok(version) => match version.version { Ok(version) => match version.version {
1 => from_str::<AppConfig>(&str), 2 => from_str::<AppConfig>(&str),
1 => from_str::<AppConfigV1>(&str).map(|c| c.into_config()),
_ => { _ => {
log::warn!("Unknown config version: {}", version.version); log::warn!("Unknown config version: {}", version.version);
None None
@@ -44,6 +49,180 @@ where T: serde::de::DeserializeOwned {
} }
} }
#[derive(serde::Deserialize, serde::Serialize)]
pub struct ScratchConfigV1 {
#[serde(default)]
pub platform: Option<String>,
#[serde(default)]
pub compiler: Option<String>,
#[serde(default)]
pub c_flags: Option<String>,
#[serde(default)]
pub ctx_path: Option<PathBuf>,
#[serde(default)]
pub build_ctx: bool,
}
impl ScratchConfigV1 {
fn into_config(self) -> ScratchConfig {
ScratchConfig {
platform: self.platform,
compiler: self.compiler,
c_flags: self.c_flags,
ctx_path: self.ctx_path,
build_ctx: self.build_ctx.then_some(true),
}
}
}
#[derive(serde::Deserialize, serde::Serialize)]
pub struct ObjectConfigV1 {
pub name: String,
pub target_path: Option<PathBuf>,
pub base_path: Option<PathBuf>,
pub reverse_fn_order: Option<bool>,
pub complete: Option<bool>,
pub scratch: Option<ScratchConfigV1>,
pub source_path: Option<String>,
}
impl ObjectConfigV1 {
fn into_config(self) -> ObjectConfig {
ObjectConfig {
name: self.name,
target_path: self.target_path,
base_path: self.base_path,
reverse_fn_order: self.reverse_fn_order,
complete: self.complete,
scratch: self.scratch.map(|scratch| scratch.into_config()),
source_path: self.source_path,
..Default::default()
}
}
}
#[derive(serde::Deserialize, serde::Serialize)]
#[serde(default)]
pub struct DiffObjConfigV1 {
pub relax_reloc_diffs: bool,
#[serde(default = "bool_true")]
pub space_between_args: bool,
pub combine_data_sections: bool,
// x86
pub x86_formatter: X86Formatter,
// MIPS
pub mips_abi: MipsAbi,
pub mips_instr_category: MipsInstrCategory,
// ARM
pub arm_arch_version: ArmArchVersion,
pub arm_unified_syntax: bool,
pub arm_av_registers: bool,
pub arm_r9_usage: ArmR9Usage,
pub arm_sl_usage: bool,
pub arm_fp_usage: bool,
pub arm_ip_usage: bool,
}
impl Default for DiffObjConfigV1 {
fn default() -> Self {
Self {
relax_reloc_diffs: false,
space_between_args: true,
combine_data_sections: false,
x86_formatter: Default::default(),
mips_abi: Default::default(),
mips_instr_category: Default::default(),
arm_arch_version: Default::default(),
arm_unified_syntax: true,
arm_av_registers: false,
arm_r9_usage: Default::default(),
arm_sl_usage: false,
arm_fp_usage: false,
arm_ip_usage: false,
}
}
}
impl DiffObjConfigV1 {
fn into_config(self) -> DiffObjConfig {
DiffObjConfig {
relax_reloc_diffs: self.relax_reloc_diffs,
space_between_args: self.space_between_args,
combine_data_sections: self.combine_data_sections,
x86_formatter: self.x86_formatter,
mips_abi: self.mips_abi,
mips_instr_category: self.mips_instr_category,
arm_arch_version: self.arm_arch_version,
arm_unified_syntax: self.arm_unified_syntax,
arm_av_registers: self.arm_av_registers,
arm_r9_usage: self.arm_r9_usage,
arm_sl_usage: self.arm_sl_usage,
arm_fp_usage: self.arm_fp_usage,
arm_ip_usage: self.arm_ip_usage,
..Default::default()
}
}
}
#[inline]
fn bool_true() -> bool { true }
#[derive(serde::Deserialize, serde::Serialize)]
pub struct AppConfigV1 {
pub version: u32,
#[serde(default)]
pub custom_make: Option<String>,
#[serde(default)]
pub custom_args: Option<Vec<String>>,
#[serde(default)]
pub selected_wsl_distro: Option<String>,
#[serde(default)]
pub project_dir: Option<PathBuf>,
#[serde(default)]
pub target_obj_dir: Option<PathBuf>,
#[serde(default)]
pub base_obj_dir: Option<PathBuf>,
#[serde(default)]
pub selected_obj: Option<ObjectConfigV1>,
#[serde(default = "bool_true")]
pub build_base: bool,
#[serde(default)]
pub build_target: bool,
#[serde(default = "bool_true")]
pub rebuild_on_changes: bool,
#[serde(default)]
pub auto_update_check: bool,
#[serde(default)]
pub watch_patterns: Vec<Glob>,
#[serde(default)]
pub recent_projects: Vec<PathBuf>,
#[serde(default)]
pub diff_obj_config: DiffObjConfigV1,
}
impl AppConfigV1 {
fn into_config(self) -> AppConfig {
log::info!("Upgrading configuration from v1");
AppConfig {
custom_make: self.custom_make,
custom_args: self.custom_args,
selected_wsl_distro: self.selected_wsl_distro,
project_dir: self.project_dir,
target_obj_dir: self.target_obj_dir,
base_obj_dir: self.base_obj_dir,
selected_obj: self.selected_obj.map(|obj| obj.into_config()),
build_base: self.build_base,
build_target: self.build_target,
rebuild_on_changes: self.rebuild_on_changes,
auto_update_check: self.auto_update_check,
watch_patterns: self.watch_patterns,
recent_projects: self.recent_projects,
diff_obj_config: self.diff_obj_config.into_config(),
..Default::default()
}
}
}
#[derive(serde::Deserialize, serde::Serialize)] #[derive(serde::Deserialize, serde::Serialize)]
pub struct ObjectConfigV0 { pub struct ObjectConfigV0 {
pub name: String, pub name: String,
@@ -59,9 +238,7 @@ impl ObjectConfigV0 {
target_path: Some(self.target_path), target_path: Some(self.target_path),
base_path: Some(self.base_path), base_path: Some(self.base_path),
reverse_fn_order: self.reverse_fn_order, reverse_fn_order: self.reverse_fn_order,
complete: None, ..Default::default()
scratch: None,
source_path: None,
} }
} }
} }

View File

@@ -4,11 +4,11 @@ use anyhow::Result;
use globset::Glob; use globset::Glob;
use objdiff_core::config::{try_project_config, ProjectObject, DEFAULT_WATCH_PATTERNS}; use objdiff_core::config::{try_project_config, ProjectObject, DEFAULT_WATCH_PATTERNS};
use crate::app::AppState; use crate::app::{AppState, ObjectConfig};
#[derive(Clone)] #[derive(Clone)]
pub enum ProjectObjectNode { pub enum ProjectObjectNode {
File(String, Box<ProjectObject>), Unit(String, usize),
Dir(String, Vec<ProjectObjectNode>), Dir(String, Vec<ProjectObjectNode>),
} }
@@ -33,17 +33,18 @@ fn find_dir<'a>(
} }
fn build_nodes( fn build_nodes(
objects: &[ProjectObject], units: &mut [ProjectObject],
project_dir: &Path, project_dir: &Path,
target_obj_dir: Option<&Path>, target_obj_dir: Option<&Path>,
base_obj_dir: Option<&Path>, base_obj_dir: Option<&Path>,
) -> Vec<ProjectObjectNode> { ) -> Vec<ProjectObjectNode> {
let mut nodes = vec![]; let mut nodes = vec![];
for object in objects { for (idx, unit) in units.iter_mut().enumerate() {
unit.resolve_paths(project_dir, target_obj_dir, base_obj_dir);
let mut out_nodes = &mut nodes; let mut out_nodes = &mut nodes;
let path = if let Some(name) = &object.name { let path = if let Some(name) = &unit.name {
Path::new(name) Path::new(name)
} else if let Some(path) = &object.path { } else if let Some(path) = &unit.path {
path path
} else { } else {
continue; continue;
@@ -56,10 +57,8 @@ fn build_nodes(
} }
} }
} }
let mut object = Box::new(object.clone());
object.resolve_paths(project_dir, target_obj_dir, base_obj_dir);
let filename = path.file_name().unwrap().to_str().unwrap().to_string(); let filename = path.file_name().unwrap().to_str().unwrap().to_string();
out_nodes.push(ProjectObjectNode::File(filename, object)); out_nodes.push(ProjectObjectNode::Unit(filename, idx));
} }
nodes nodes
} }
@@ -70,24 +69,36 @@ pub fn load_project_config(state: &mut AppState) -> Result<()> {
}; };
if let Some((result, info)) = try_project_config(project_dir) { if let Some((result, info)) = try_project_config(project_dir) {
let project_config = result?; let project_config = result?;
state.config.custom_make = project_config.custom_make; state.config.custom_make = project_config.custom_make.clone();
state.config.custom_args = project_config.custom_args; state.config.custom_args = project_config.custom_args.clone();
state.config.target_obj_dir = project_config.target_dir.map(|p| project_dir.join(p)); state.config.target_obj_dir =
state.config.base_obj_dir = project_config.base_dir.map(|p| project_dir.join(p)); project_config.target_dir.as_deref().map(|p| project_dir.join(p));
state.config.build_base = project_config.build_base; state.config.base_obj_dir = project_config.base_dir.as_deref().map(|p| project_dir.join(p));
state.config.build_target = project_config.build_target; state.config.build_base = project_config.build_base.unwrap_or(true);
state.config.watch_patterns = project_config.watch_patterns.unwrap_or_else(|| { state.config.build_target = project_config.build_target.unwrap_or(false);
state.config.watch_patterns = project_config.watch_patterns.clone().unwrap_or_else(|| {
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect() DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect()
}); });
state.watcher_change = true; state.watcher_change = true;
state.objects = project_config.objects; state.objects = project_config.units.clone().unwrap_or_default();
state.object_nodes = build_nodes( state.object_nodes = build_nodes(
&state.objects, &mut state.objects,
project_dir, project_dir,
state.config.target_obj_dir.as_deref(), state.config.target_obj_dir.as_deref(),
state.config.base_obj_dir.as_deref(), state.config.base_obj_dir.as_deref(),
); );
state.current_project_config = Some(project_config);
state.project_config_info = Some(info); state.project_config_info = Some(info);
// Reload selected object
if let Some(selected_obj) = &state.config.selected_obj {
if let Some(obj) = state.objects.iter().find(|o| o.name() == selected_obj.name) {
let config = ObjectConfig::from(obj);
state.set_selected_obj(config);
} else {
state.clear_selected_obj();
}
}
} }
Ok(()) Ok(())
} }

View File

@@ -39,7 +39,7 @@ impl CreateScratchConfig {
Ok(Self { Ok(Self {
build_config: BuildConfig::from_config(config), build_config: BuildConfig::from_config(config),
context_path: scratch_config.ctx_path.clone(), context_path: scratch_config.ctx_path.clone(),
build_context: scratch_config.build_ctx, build_context: scratch_config.build_ctx.unwrap_or(false),
compiler: scratch_config.compiler.clone().unwrap_or_default(), compiler: scratch_config.compiler.clone().unwrap_or_default(),
platform: scratch_config.platform.clone().unwrap_or_default(), platform: scratch_config.platform.clone().unwrap_or_default(),
compiler_flags: scratch_config.c_flags.clone().unwrap_or_default(), compiler_flags: scratch_config.c_flags.clone().unwrap_or_default(),

View File

@@ -53,7 +53,7 @@ impl JobQueue {
} }
/// Returns whether any job is running. /// Returns whether any job is running.
#[allow(dead_code)] #[expect(dead_code)]
pub fn any_running(&self) -> bool { pub fn any_running(&self) -> bool {
self.jobs.iter().any(|job| { self.jobs.iter().any(|job| {
if let Some(handle) = &job.handle { if let Some(handle) = &job.handle {

View File

@@ -6,13 +6,13 @@ use std::{
use anyhow::{anyhow, Error, Result}; use anyhow::{anyhow, Error, Result};
use objdiff_core::{ use objdiff_core::{
diff::{diff_objs, DiffObjConfig, ObjDiff}, diff::{diff_objs, DiffObjConfig, MappingConfig, ObjDiff},
obj::{read, ObjInfo}, obj::{read, ObjInfo},
}; };
use time::OffsetDateTime; use time::OffsetDateTime;
use crate::{ use crate::{
app::{AppConfig, ObjectConfig}, app::{AppConfig, AppState, ObjectConfig},
jobs::{start_job, update_status, Job, JobContext, JobResult, JobState}, jobs::{start_job, update_status, Job, JobContext, JobResult, JobState},
}; };
@@ -60,16 +60,20 @@ pub struct ObjDiffConfig {
pub build_target: bool, pub build_target: bool,
pub selected_obj: Option<ObjectConfig>, pub selected_obj: Option<ObjectConfig>,
pub diff_obj_config: DiffObjConfig, pub diff_obj_config: DiffObjConfig,
pub selecting_left: Option<String>,
pub selecting_right: Option<String>,
} }
impl ObjDiffConfig { impl ObjDiffConfig {
pub(crate) fn from_config(config: &AppConfig) -> Self { pub(crate) fn from_state(state: &AppState) -> Self {
Self { Self {
build_config: BuildConfig::from_config(config), build_config: BuildConfig::from_config(&state.config),
build_base: config.build_base, build_base: state.config.build_base,
build_target: config.build_target, build_target: state.config.build_target,
selected_obj: config.selected_obj.clone(), selected_obj: state.config.selected_obj.clone(),
diff_obj_config: config.diff_obj_config.clone(), diff_obj_config: state.config.diff_obj_config.clone(),
selecting_left: state.selecting_left.clone(),
selecting_right: state.selecting_right.clone(),
} }
} }
} }
@@ -158,9 +162,16 @@ pub(crate) fn run_make(config: &BuildConfig, arg: &Path) -> BuildStatus {
fn run_build( fn run_build(
context: &JobContext, context: &JobContext,
cancel: Receiver<()>, cancel: Receiver<()>,
config: ObjDiffConfig, mut config: ObjDiffConfig,
) -> Result<Box<ObjDiffResult>> { ) -> Result<Box<ObjDiffResult>> {
let obj_config = config.selected_obj.as_ref().ok_or_else(|| Error::msg("Missing obj path"))?; let obj_config = config.selected_obj.ok_or_else(|| Error::msg("Missing obj path"))?;
// Use the per-object symbol mappings, we don't set mappings globally
config.diff_obj_config.symbol_mappings = MappingConfig {
mappings: obj_config.symbol_mappings,
selecting_left: config.selecting_left,
selecting_right: config.selecting_right,
};
let project_dir = config let project_dir = config
.build_config .build_config
.project_dir .project_dir

View File

@@ -205,7 +205,7 @@ pub const DEFAULT_COLOR_ROTATION: [Color32; 9] = [
Color32::from_rgb(255, 0, 0), Color32::from_rgb(255, 0, 0),
Color32::from_rgb(255, 255, 0), Color32::from_rgb(255, 255, 0),
Color32::from_rgb(255, 192, 203), Color32::from_rgb(255, 192, 203),
Color32::from_rgb(0, 0, 255), Color32::from_rgb(128, 128, 255),
Color32::from_rgb(0, 255, 0), Color32::from_rgb(0, 255, 0),
Color32::from_rgb(213, 138, 138), Color32::from_rgb(213, 138, 138),
]; ];

View File

@@ -0,0 +1,82 @@
use egui::{Align, Layout, Sense, Vec2};
use egui_extras::{Column, Size, StripBuilder, TableBuilder, TableRow};
pub fn render_header(
ui: &mut egui::Ui,
available_width: f32,
num_columns: usize,
mut add_contents: impl FnMut(&mut egui::Ui, usize),
) {
let column_width = available_width / num_columns as f32;
ui.allocate_ui_with_layout(
Vec2 { x: available_width, y: 100.0 },
Layout::left_to_right(Align::Min),
|ui| {
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate);
for i in 0..num_columns {
ui.allocate_ui_with_layout(
Vec2 { x: column_width, y: 100.0 },
Layout::top_down(Align::Min),
|ui| {
ui.set_width(column_width);
add_contents(ui, i);
},
);
}
},
);
ui.separator();
}
pub fn render_table(
ui: &mut egui::Ui,
available_width: f32,
num_columns: usize,
row_height: f32,
total_rows: usize,
mut add_contents: impl FnMut(&mut TableRow, usize),
) {
ui.style_mut().interaction.selectable_labels = false;
let column_width = available_width / num_columns as f32;
let available_height = ui.available_height();
let table = TableBuilder::new(ui)
.striped(false)
.cell_layout(Layout::left_to_right(Align::Min))
.columns(Column::exact(column_width).clip(true), num_columns)
.resizable(false)
.auto_shrink([false, false])
.min_scrolled_height(available_height)
.sense(Sense::click());
table.body(|body| {
body.rows(row_height, total_rows, |mut row| {
row.set_hovered(false); // Disable hover effect
for i in 0..num_columns {
add_contents(&mut row, i);
}
});
});
}
pub fn render_strips(
ui: &mut egui::Ui,
available_width: f32,
num_columns: usize,
mut add_contents: impl FnMut(&mut egui::Ui, usize),
) {
let column_width = available_width / num_columns as f32;
StripBuilder::new(ui).size(Size::remainder()).clip(true).vertical(|mut strip| {
strip.strip(|builder| {
builder.sizes(Size::exact(column_width), num_columns).clip(true).horizontal(
|mut strip| {
for i in 0..num_columns {
strip.cell(|ui| {
ui.push_id(i, |ui| {
add_contents(ui, i);
});
});
}
},
);
});
});
}

View File

@@ -43,7 +43,6 @@ pub struct ConfigViewState {
pub build_running: bool, pub build_running: bool,
pub queue_build: bool, pub queue_build: bool,
pub watch_pattern_text: String, pub watch_pattern_text: String,
pub load_error: Option<String>,
pub object_search: String, pub object_search: String,
pub filter_diffable: bool, pub filter_diffable: bool,
pub filter_incomplete: bool, pub filter_incomplete: bool,
@@ -93,10 +92,7 @@ impl ConfigViewState {
name: obj_path.display().to_string(), name: obj_path.display().to_string(),
target_path: Some(target_path), target_path: Some(target_path),
base_path: Some(path), base_path: Some(path),
reverse_fn_order: None, ..Default::default()
complete: None,
scratch: None,
source_path: None,
}); });
} else if let Ok(obj_path) = path.strip_prefix(target_dir) { } else if let Ok(obj_path) = path.strip_prefix(target_dir) {
let base_path = base_dir.join(obj_path); let base_path = base_dir.join(obj_path);
@@ -104,10 +100,7 @@ impl ConfigViewState {
name: obj_path.display().to_string(), name: obj_path.display().to_string(),
target_path: Some(path), target_path: Some(path),
base_path: Some(base_path), base_path: Some(base_path),
reverse_fn_order: None, ..Default::default()
complete: None,
scratch: None,
source_path: None,
}); });
} }
} }
@@ -230,7 +223,10 @@ pub fn config_ui(
} }
}); });
let mut new_selected_obj = selected_obj.clone(); let selected_index = selected_obj.as_ref().and_then(|selected_obj| {
objects.iter().position(|obj| obj.name.as_ref() == Some(&selected_obj.name))
});
let mut new_selected_index = selected_index;
if objects.is_empty() { if objects.is_empty() {
if let (Some(_base_dir), Some(target_dir)) = (base_obj_dir, target_obj_dir) { if let (Some(_base_dir), Some(target_dir)) = (base_obj_dir, target_obj_dir) {
if ui.button("Select object").clicked() { if ui.button("Select object").clicked() {
@@ -316,6 +312,7 @@ pub fn config_ui(
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
for node in object_nodes.iter().filter_map(|node| { for node in object_nodes.iter().filter_map(|node| {
filter_node( filter_node(
objects,
node, node,
&search, &search,
config_state.filter_diffable, config_state.filter_diffable,
@@ -325,8 +322,9 @@ pub fn config_ui(
}) { }) {
display_node( display_node(
ui, ui,
&mut new_selected_obj, &mut new_selected_index,
project_dir.as_deref(), project_dir.as_deref(),
objects,
&node, &node,
appearance, appearance,
node_open, node_open,
@@ -334,10 +332,11 @@ pub fn config_ui(
} }
}); });
} }
if new_selected_obj != *selected_obj { if new_selected_index != selected_index {
if let Some(obj) = new_selected_obj { if let Some(idx) = new_selected_index {
// Will set obj_changed, which will trigger a rebuild // Will set obj_changed, which will trigger a rebuild
state_guard.set_selected_obj(obj); let config = ObjectConfig::from(&objects[idx]);
state_guard.set_selected_obj(config);
} }
} }
if state_guard.config.selected_obj.is_some() if state_guard.config.selected_obj.is_some()
@@ -347,16 +346,17 @@ pub fn config_ui(
} }
} }
fn display_object( fn display_unit(
ui: &mut egui::Ui, ui: &mut egui::Ui,
selected_obj: &mut Option<ObjectConfig>, selected_obj: &mut Option<usize>,
project_dir: Option<&Path>, project_dir: Option<&Path>,
name: &str, name: &str,
object: &ProjectObject, units: &[ProjectObject],
index: usize,
appearance: &Appearance, appearance: &Appearance,
) { ) {
let object_name = object.name(); let object = &units[index];
let selected = matches!(selected_obj, Some(obj) if obj.name == object_name); let selected = *selected_obj == Some(index);
let color = if selected { let color = if selected {
appearance.emphasized_text_color appearance.emphasized_text_color
} else if let Some(complete) = object.complete() { } else if let Some(complete) = object.complete() {
@@ -381,18 +381,8 @@ fn display_object(
if get_source_path(project_dir, object).is_some() { if get_source_path(project_dir, object).is_some() {
response.context_menu(|ui| object_context_ui(ui, object, project_dir)); response.context_menu(|ui| object_context_ui(ui, object, project_dir));
} }
// Always recreate ObjectConfig if selected, in case the project config changed. if response.clicked() {
// ObjectConfig is compared using equality, so this won't unnecessarily trigger a rebuild. *selected_obj = Some(index);
if selected || response.clicked() {
*selected_obj = Some(ObjectConfig {
name: object_name.to_string(),
target_path: object.target_path.clone(),
base_path: object.base_path.clone(),
reverse_fn_order: object.reverse_fn_order(),
complete: object.complete(),
scratch: object.scratch.clone(),
source_path: object.source_path().cloned(),
});
} }
} }
@@ -427,18 +417,19 @@ enum NodeOpen {
fn display_node( fn display_node(
ui: &mut egui::Ui, ui: &mut egui::Ui,
selected_obj: &mut Option<ObjectConfig>, selected_obj: &mut Option<usize>,
project_dir: Option<&Path>, project_dir: Option<&Path>,
units: &[ProjectObject],
node: &ProjectObjectNode, node: &ProjectObjectNode,
appearance: &Appearance, appearance: &Appearance,
node_open: NodeOpen, node_open: NodeOpen,
) { ) {
match node { match node {
ProjectObjectNode::File(name, object) => { ProjectObjectNode::Unit(name, idx) => {
display_object(ui, selected_obj, project_dir, name, object, appearance); display_unit(ui, selected_obj, project_dir, name, units, *idx, appearance);
} }
ProjectObjectNode::Dir(name, children) => { ProjectObjectNode::Dir(name, children) => {
let contains_obj = selected_obj.as_ref().map(|path| contains_node(node, path)); let contains_obj = selected_obj.map(|idx| contains_node(node, idx));
let open = match node_open { let open = match node_open {
NodeOpen::Default => None, NodeOpen::Default => None,
NodeOpen::Open => Some(true), NodeOpen::Open => Some(true),
@@ -461,16 +452,16 @@ fn display_node(
.open(open) .open(open)
.show(ui, |ui| { .show(ui, |ui| {
for node in children { for node in children {
display_node(ui, selected_obj, project_dir, node, appearance, node_open); display_node(ui, selected_obj, project_dir, units, node, appearance, node_open);
} }
}); });
} }
} }
} }
fn contains_node(node: &ProjectObjectNode, selected_obj: &ObjectConfig) -> bool { fn contains_node(node: &ProjectObjectNode, selected_obj: usize) -> bool {
match node { match node {
ProjectObjectNode::File(_, object) => object.name() == selected_obj.name, ProjectObjectNode::Unit(_, idx) => *idx == selected_obj,
ProjectObjectNode::Dir(_, children) => { ProjectObjectNode::Dir(_, children) => {
children.iter().any(|node| contains_node(node, selected_obj)) children.iter().any(|node| contains_node(node, selected_obj))
} }
@@ -478,6 +469,7 @@ fn contains_node(node: &ProjectObjectNode, selected_obj: &ObjectConfig) -> bool
} }
fn filter_node( fn filter_node(
units: &[ProjectObject],
node: &ProjectObjectNode, node: &ProjectObjectNode,
search: &str, search: &str,
filter_diffable: bool, filter_diffable: bool,
@@ -485,12 +477,12 @@ fn filter_node(
show_hidden: bool, show_hidden: bool,
) -> Option<ProjectObjectNode> { ) -> Option<ProjectObjectNode> {
match node { match node {
ProjectObjectNode::File(name, object) => { ProjectObjectNode::Unit(name, idx) => {
let unit = &units[*idx];
if (search.is_empty() || name.to_ascii_lowercase().contains(search)) if (search.is_empty() || name.to_ascii_lowercase().contains(search))
&& (!filter_diffable && (!filter_diffable || (unit.base_path.is_some() && unit.target_path.is_some()))
|| (object.base_path.is_some() && object.target_path.is_some())) && (!filter_incomplete || matches!(unit.complete(), None | Some(false)))
&& (!filter_incomplete || matches!(object.complete(), None | Some(false))) && (show_hidden || !unit.hidden())
&& (show_hidden || !object.hidden())
{ {
Some(node.clone()) Some(node.clone())
} else { } else {
@@ -501,7 +493,14 @@ fn filter_node(
let new_children = children let new_children = children
.iter() .iter()
.filter_map(|child| { .filter_map(|child| {
filter_node(child, search, filter_diffable, filter_incomplete, show_hidden) filter_node(
units,
child,
search,
filter_diffable,
filter_incomplete,
show_hidden,
)
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
if !new_children.is_empty() { if !new_children.is_empty() {
@@ -570,14 +569,14 @@ pub fn project_window(
split_obj_config_ui(ui, &mut state_guard, config_state, appearance); split_obj_config_ui(ui, &mut state_guard, config_state, appearance);
}); });
if let Some(error) = &config_state.load_error { if let Some(error) = &state_guard.config_error {
let mut open = true; let mut open = true;
egui::Window::new("Error").open(&mut open).show(ctx, |ui| { egui::Window::new("Error").open(&mut open).show(ctx, |ui| {
ui.label("Failed to load project config:"); ui.label("Failed to load project config:");
ui.colored_label(appearance.delete_color, error); ui.colored_label(appearance.delete_color, error);
}); });
if !open { if !open {
config_state.load_error = None; state_guard.config_error = None;
} }
} }
} }

View File

@@ -1,7 +1,6 @@
use std::{cmp::min, default::Default, mem::take}; use std::{cmp::min, default::Default, mem::take};
use egui::{text::LayoutJob, Align, Label, Layout, Sense, Vec2, Widget}; use egui::{text::LayoutJob, Id, Label, RichText, Sense, Widget};
use egui_extras::{Column, TableBuilder};
use objdiff_core::{ use objdiff_core::{
diff::{ObjDataDiff, ObjDataDiffKind, ObjDiff}, diff::{ObjDataDiff, ObjDataDiffKind, ObjDiff},
obj::ObjInfo, obj::ObjInfo,
@@ -10,14 +9,15 @@ use time::format_description;
use crate::views::{ use crate::views::{
appearance::Appearance, appearance::Appearance,
symbol_diff::{DiffViewState, SymbolRefByName, View}, column_layout::{render_header, render_table},
symbol_diff::{DiffViewAction, DiffViewNavigation, DiffViewState},
write_text, write_text,
}; };
const BYTES_PER_ROW: usize = 16; const BYTES_PER_ROW: usize = 16;
fn find_section(obj: &ObjInfo, selected_symbol: &SymbolRefByName) -> Option<usize> { fn find_section(obj: &ObjInfo, section_name: &str) -> Option<usize> {
obj.sections.iter().position(|section| section.name == selected_symbol.section_name) obj.sections.iter().position(|section| section.name == section_name)
} }
fn data_row_ui(ui: &mut egui::Ui, address: usize, diffs: &[ObjDataDiff], appearance: &Appearance) { fn data_row_ui(ui: &mut egui::Ui, address: usize, diffs: &[ObjDataDiff], appearance: &Appearance) {
@@ -131,20 +131,37 @@ fn split_diffs(diffs: &[ObjDataDiff]) -> Vec<Vec<ObjDataDiff>> {
split_diffs split_diffs
} }
#[derive(Clone, Copy)]
struct SectionDiffContext<'a> {
obj: &'a ObjInfo,
diff: &'a ObjDiff,
section_index: Option<usize>,
}
impl<'a> SectionDiffContext<'a> {
pub fn new(obj: Option<&'a (ObjInfo, ObjDiff)>, section_name: Option<&str>) -> Option<Self> {
obj.map(|(obj, diff)| Self {
obj,
diff,
section_index: section_name.and_then(|section_name| find_section(obj, section_name)),
})
}
#[inline]
pub fn has_section(&self) -> bool { self.section_index.is_some() }
}
fn data_table_ui( fn data_table_ui(
table: TableBuilder<'_>, ui: &mut egui::Ui,
left_obj: Option<&(ObjInfo, ObjDiff)>, available_width: f32,
right_obj: Option<&(ObjInfo, ObjDiff)>, left_ctx: Option<SectionDiffContext<'_>>,
selected_symbol: &SymbolRefByName, right_ctx: Option<SectionDiffContext<'_>>,
config: &Appearance, config: &Appearance,
) -> Option<()> { ) -> Option<()> {
let left_section = left_obj.and_then(|(obj, diff)| { let left_section = left_ctx
find_section(obj, selected_symbol).map(|i| (&obj.sections[i], &diff.sections[i])) .and_then(|ctx| ctx.section_index.map(|i| (&ctx.obj.sections[i], &ctx.diff.sections[i])));
}); let right_section = right_ctx
let right_section = right_obj.and_then(|(obj, diff)| { .and_then(|ctx| ctx.section_index.map(|i| (&ctx.obj.sections[i], &ctx.diff.sections[i])));
find_section(obj, selected_symbol).map(|i| (&obj.sections[i], &diff.sections[i]))
});
let total_bytes = left_section let total_bytes = left_section
.or(right_section)? .or(right_section)?
.1 .1
@@ -159,118 +176,117 @@ fn data_table_ui(
let left_diffs = left_section.map(|(_, section)| split_diffs(&section.data_diff)); let left_diffs = left_section.map(|(_, section)| split_diffs(&section.data_diff));
let right_diffs = right_section.map(|(_, section)| split_diffs(&section.data_diff)); let right_diffs = right_section.map(|(_, section)| split_diffs(&section.data_diff));
table.body(|body| { render_table(ui, available_width, 2, config.code_font.size, total_rows, |row, column| {
body.rows(config.code_font.size, total_rows, |mut row| { let i = row.index();
let row_index = row.index(); let address = i * BYTES_PER_ROW;
let address = row_index * BYTES_PER_ROW; row.col(|ui| {
row.col(|ui| { if column == 0 {
if let Some(left_diffs) = &left_diffs { if let Some(left_diffs) = &left_diffs {
data_row_ui(ui, address, &left_diffs[row_index], config); data_row_ui(ui, address, &left_diffs[i], config);
} }
}); } else if column == 1 {
row.col(|ui| {
if let Some(right_diffs) = &right_diffs { if let Some(right_diffs) = &right_diffs {
data_row_ui(ui, address, &right_diffs[row_index], config); data_row_ui(ui, address, &right_diffs[i], config);
} }
}); }
}); });
}); });
Some(()) Some(())
} }
pub fn data_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &Appearance) { #[must_use]
let (Some(result), Some(selected_symbol)) = (&state.build, &state.symbol_state.selected_symbol) pub fn data_diff_ui(
else { ui: &mut egui::Ui,
return; state: &DiffViewState,
appearance: &Appearance,
) -> Option<DiffViewAction> {
let mut ret = None;
let Some(result) = &state.build else {
return ret;
}; };
let section_name =
state.symbol_state.left_symbol.as_ref().and_then(|s| s.section_name.as_deref()).or_else(
|| state.symbol_state.right_symbol.as_ref().and_then(|s| s.section_name.as_deref()),
);
let left_ctx = SectionDiffContext::new(result.first_obj.as_ref(), section_name);
let right_ctx = SectionDiffContext::new(result.second_obj.as_ref(), section_name);
// If both sides are missing a symbol, switch to symbol diff view
if !right_ctx.map_or(false, |ctx| ctx.has_section())
&& !left_ctx.map_or(false, |ctx| ctx.has_section())
{
return Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
}
// Header // Header
let available_width = ui.available_width(); let available_width = ui.available_width();
let column_width = available_width / 2.0; render_header(ui, available_width, 2, |ui, column| {
ui.allocate_ui_with_layout( if column == 0 {
Vec2 { x: available_width, y: 100.0 },
Layout::left_to_right(Align::Min),
|ui| {
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate);
// Left column // Left column
ui.allocate_ui_with_layout( if ui.button("⏴ Back").clicked() {
Vec2 { x: column_width, y: 100.0 }, ret = Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
Layout::top_down(Align::Min), }
|ui| {
ui.set_width(column_width);
if ui.button("⏴ Back").clicked() {
state.current_view = View::SymbolDiff;
}
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.colored_label(appearance.highlight_color, &selected_symbol.symbol_name);
ui.label("Diff target:");
});
},
);
if let Some(section) =
left_ctx.and_then(|ctx| ctx.section_index.map(|i| &ctx.obj.sections[i]))
{
ui.label(
RichText::new(section.name.clone())
.font(appearance.code_font.clone())
.color(appearance.highlight_color),
);
} else {
ui.label(
RichText::new("Missing")
.font(appearance.code_font.clone())
.color(appearance.replace_color),
);
}
} else if column == 1 {
// Right column // Right column
ui.allocate_ui_with_layout( ui.horizontal(|ui| {
Vec2 { x: column_width, y: 100.0 }, if ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() {
Layout::top_down(Align::Min), ret = Some(DiffViewAction::Build);
|ui| { }
ui.set_width(column_width); ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
if state.build_running {
ui.colored_label(appearance.replace_color, "Building…");
} else {
ui.label("Last built:");
let format = format_description::parse("[hour]:[minute]:[second]").unwrap();
ui.label(
result.time.to_offset(appearance.utc_offset).format(&format).unwrap(),
);
}
});
});
ui.horizontal(|ui| { if let Some(section) =
if ui right_ctx.and_then(|ctx| ctx.section_index.map(|i| &ctx.obj.sections[i]))
.add_enabled(!state.build_running, egui::Button::new("Build")) {
.clicked() ui.label(
{ RichText::new(section.name.clone())
state.queue_build = true; .font(appearance.code_font.clone())
} .color(appearance.highlight_color),
ui.scope(|ui| { );
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); } else {
if state.build_running { ui.label(
ui.colored_label(appearance.replace_color, "Building"); RichText::new("Missing")
} else { .font(appearance.code_font.clone())
ui.label("Last built:"); .color(appearance.replace_color),
let format = );
format_description::parse("[hour]:[minute]:[second]").unwrap(); }
ui.label( }
result });
.time
.to_offset(appearance.utc_offset)
.format(&format)
.unwrap(),
);
}
});
});
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.label("");
ui.label("Diff base:");
});
},
);
},
);
ui.separator();
// Table // Table
ui.style_mut().interaction.selectable_labels = false; let id =
let available_height = ui.available_height(); Id::new(state.symbol_state.left_symbol.as_ref().and_then(|s| s.section_name.as_deref()))
let table = TableBuilder::new(ui) .with(state.symbol_state.right_symbol.as_ref().and_then(|s| s.section_name.as_deref()));
.striped(false) ui.push_id(id, |ui| {
.cell_layout(Layout::left_to_right(Align::Min)) data_table_ui(ui, available_width, left_ctx, right_ctx, appearance);
.columns(Column::exact(column_width).clip(true), 2) });
.resizable(false) ret
.auto_shrink([false, false])
.min_scrolled_height(available_height);
data_table_ui(
table,
result.first_obj.as_ref(),
result.second_obj.as_ref(),
selected_symbol,
appearance,
);
} }

View File

@@ -1,28 +1,20 @@
use egui::{Align, Layout, ScrollArea, Ui, Vec2}; use egui::{RichText, ScrollArea};
use egui_extras::{Size, StripBuilder};
use objdiff_core::{ use objdiff_core::{
arch::ppc::ExceptionInfo, arch::ppc::ExceptionInfo,
diff::ObjDiff, obj::{ObjInfo, ObjSymbol},
obj::{ObjInfo, ObjSymbol, SymbolRef},
}; };
use time::format_description; use time::format_description;
use crate::views::{ use crate::views::{
appearance::Appearance, appearance::Appearance,
symbol_diff::{match_color_for_symbol, DiffViewState, SymbolRefByName, View}, column_layout::{render_header, render_strips},
function_diff::FunctionDiffContext,
symbol_diff::{
match_color_for_symbol, DiffViewAction, DiffViewNavigation, DiffViewState, SymbolRefByName,
View,
},
}; };
fn find_symbol(obj: &ObjInfo, selected_symbol: &SymbolRefByName) -> Option<SymbolRef> {
for (section_idx, section) in obj.sections.iter().enumerate() {
for (symbol_idx, symbol) in section.symbols.iter().enumerate() {
if symbol.name == selected_symbol.symbol_name {
return Some(SymbolRef { section_idx, symbol_idx });
}
}
}
None
}
fn decode_extab(extab: &ExceptionInfo) -> String { fn decode_extab(extab: &ExceptionInfo) -> String {
let mut text = String::from(""); let mut text = String::from("");
@@ -48,14 +40,12 @@ fn find_extab_entry<'a>(obj: &'a ObjInfo, symbol: &ObjSymbol) -> Option<&'a Exce
} }
fn extab_text_ui( fn extab_text_ui(
ui: &mut Ui, ui: &mut egui::Ui,
obj: &(ObjInfo, ObjDiff), ctx: FunctionDiffContext<'_>,
symbol_ref: SymbolRef, symbol: &ObjSymbol,
appearance: &Appearance, appearance: &Appearance,
) -> Option<()> { ) -> Option<()> {
let (_section, symbol) = obj.0.section_symbol(symbol_ref); if let Some(extab_entry) = find_extab_entry(ctx.obj, symbol) {
if let Some(extab_entry) = find_extab_entry(&obj.0, symbol) {
let text = decode_extab(extab_entry); let text = decode_extab(extab_entry);
ui.colored_label(appearance.replace_color, &text); ui.colored_label(appearance.replace_color, &text);
return Some(()); return Some(());
@@ -65,137 +55,194 @@ fn extab_text_ui(
} }
fn extab_ui( fn extab_ui(
ui: &mut Ui, ui: &mut egui::Ui,
obj: Option<&(ObjInfo, ObjDiff)>, ctx: FunctionDiffContext<'_>,
selected_symbol: &SymbolRefByName,
appearance: &Appearance, appearance: &Appearance,
_left: bool, _column: usize,
) { ) {
ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| { ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| {
ui.scope(|ui| { ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
let symbol = obj.and_then(|(obj, _)| find_symbol(obj, selected_symbol)); if let Some((_section, symbol)) =
ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref))
if let (Some(object), Some(symbol_ref)) = (obj, symbol) { {
extab_text_ui(ui, object, symbol_ref, appearance); extab_text_ui(ui, ctx, symbol, appearance);
} }
}); });
}); });
} }
pub fn extab_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &Appearance) { #[must_use]
let (Some(result), Some(selected_symbol)) = (&state.build, &state.symbol_state.selected_symbol) pub fn extab_diff_ui(
else { ui: &mut egui::Ui,
return; state: &DiffViewState,
appearance: &Appearance,
) -> Option<DiffViewAction> {
let mut ret = None;
let Some(result) = &state.build else {
return ret;
}; };
let mut left_ctx = FunctionDiffContext::new(
result.first_obj.as_ref(),
state.symbol_state.left_symbol.as_ref(),
);
let mut right_ctx = FunctionDiffContext::new(
result.second_obj.as_ref(),
state.symbol_state.right_symbol.as_ref(),
);
// If one side is missing a symbol, but the diff process found a match, use that symbol
let left_diff_symbol = left_ctx.and_then(|ctx| {
ctx.symbol_ref.and_then(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).target_symbol)
});
let right_diff_symbol = right_ctx.and_then(|ctx| {
ctx.symbol_ref.and_then(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).target_symbol)
});
if left_diff_symbol.is_some() && right_ctx.map_or(false, |ctx| !ctx.has_symbol()) {
let (right_section, right_symbol) =
right_ctx.unwrap().obj.section_symbol(left_diff_symbol.unwrap());
let symbol_ref = SymbolRefByName::new(right_symbol, right_section);
right_ctx = FunctionDiffContext::new(result.second_obj.as_ref(), Some(&symbol_ref));
ret = Some(DiffViewAction::Navigate(DiffViewNavigation {
view: Some(View::FunctionDiff),
left_symbol: state.symbol_state.left_symbol.clone(),
right_symbol: Some(symbol_ref),
}));
} else if right_diff_symbol.is_some() && left_ctx.map_or(false, |ctx| !ctx.has_symbol()) {
let (left_section, left_symbol) =
left_ctx.unwrap().obj.section_symbol(right_diff_symbol.unwrap());
let symbol_ref = SymbolRefByName::new(left_symbol, left_section);
left_ctx = FunctionDiffContext::new(result.first_obj.as_ref(), Some(&symbol_ref));
ret = Some(DiffViewAction::Navigate(DiffViewNavigation {
view: Some(View::FunctionDiff),
left_symbol: Some(symbol_ref),
right_symbol: state.symbol_state.right_symbol.clone(),
}));
}
// If both sides are missing a symbol, switch to symbol diff view
if right_ctx.map_or(false, |ctx| !ctx.has_symbol())
&& left_ctx.map_or(false, |ctx| !ctx.has_symbol())
{
return Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
}
// Header // Header
let available_width = ui.available_width(); let available_width = ui.available_width();
let column_width = available_width / 2.0; render_header(ui, available_width, 2, |ui, column| {
ui.allocate_ui_with_layout( if column == 0 {
Vec2 { x: available_width, y: 100.0 },
Layout::left_to_right(Align::Min),
|ui| {
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate);
// Left column // Left column
ui.allocate_ui_with_layout( ui.horizontal(|ui| {
Vec2 { x: column_width, y: 100.0 }, if ui.button("⏴ Back").clicked() {
Layout::top_down(Align::Min), ret = Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
|ui| { }
ui.set_width(column_width); ui.separator();
if ui
ui.horizontal(|ui| { .add_enabled(
if ui.button("⏴ Back").clicked() { !state.scratch_running
state.current_view = View::SymbolDiff; && state.scratch_available
} && left_ctx.map_or(false, |ctx| ctx.has_symbol()),
}); egui::Button::new("📲 decomp.me"),
)
let name = selected_symbol .on_hover_text_at_pointer("Create a new scratch on decomp.me (beta)")
.demangled_symbol_name .on_disabled_hover_text("Scratch configuration missing")
.as_deref() .clicked()
.unwrap_or(&selected_symbol.symbol_name); {
ui.scope(|ui| { if let Some((_section, symbol)) = left_ctx.and_then(|ctx| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref))
ui.colored_label(appearance.highlight_color, name); }) {
ui.label("Diff target:"); ret = Some(DiffViewAction::CreateScratch(symbol.name.clone()));
}); }
}, }
); });
if let Some((_section, symbol)) = left_ctx
.and_then(|ctx| ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref)))
{
let name = symbol.demangled_name.as_deref().unwrap_or(&symbol.name);
ui.label(
RichText::new(name)
.font(appearance.code_font.clone())
.color(appearance.highlight_color),
);
} else {
ui.label(
RichText::new("Missing")
.font(appearance.code_font.clone())
.color(appearance.replace_color),
);
}
} else if column == 1 {
// Right column // Right column
ui.allocate_ui_with_layout( ui.horizontal(|ui| {
Vec2 { x: column_width, y: 100.0 }, if ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() {
Layout::top_down(Align::Min), ret = Some(DiffViewAction::Build);
|ui| { }
ui.set_width(column_width); ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
if state.build_running {
ui.colored_label(appearance.replace_color, "Building…");
} else {
ui.label("Last built:");
let format = format_description::parse("[hour]:[minute]:[second]").unwrap();
ui.label(
result.time.to_offset(appearance.utc_offset).format(&format).unwrap(),
);
}
});
ui.separator();
if ui
.add_enabled(state.source_path_available, egui::Button::new("🖹 Source file"))
.on_hover_text_at_pointer("Open the source file in the default editor")
.on_disabled_hover_text("Source file metadata missing")
.clicked()
{
ret = Some(DiffViewAction::OpenSourcePath);
}
});
ui.horizontal(|ui| { if let Some(((_section, symbol), symbol_diff)) = right_ctx.and_then(|ctx| {
if ui ctx.symbol_ref.map(|symbol_ref| {
.add_enabled(!state.build_running, egui::Button::new("Build")) (ctx.obj.section_symbol(symbol_ref), ctx.diff.symbol_diff(symbol_ref))
.clicked() })
{ }) {
state.queue_build = true; let name = symbol.demangled_name.as_deref().unwrap_or(&symbol.name);
} ui.label(
ui.scope(|ui| { RichText::new(name)
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); .font(appearance.code_font.clone())
if state.build_running { .color(appearance.highlight_color),
ui.colored_label(appearance.replace_color, "Building…"); );
} else { if let Some(match_percent) = symbol_diff.match_percent {
ui.label("Last built:"); ui.label(
let format = RichText::new(format!("{:.0}%", match_percent.floor()))
format_description::parse("[hour]:[minute]:[second]").unwrap(); .font(appearance.code_font.clone())
ui.label( .color(match_color_for_symbol(match_percent, appearance)),
result );
.time }
.to_offset(appearance.utc_offset) } else {
.format(&format) ui.label(
.unwrap(), RichText::new("Missing")
); .font(appearance.code_font.clone())
} .color(appearance.replace_color),
}); );
}); }
}
ui.scope(|ui| { });
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
if let Some(match_percent) = result
.second_obj
.as_ref()
.and_then(|(obj, diff)| {
find_symbol(obj, selected_symbol).map(|sref| {
&diff.sections[sref.section_idx].symbols[sref.symbol_idx]
})
})
.and_then(|symbol| symbol.match_percent)
{
ui.colored_label(
match_color_for_symbol(match_percent, appearance),
format!("{match_percent:.0}%"),
);
} else {
ui.colored_label(appearance.replace_color, "Missing");
}
ui.label("Diff base:");
});
},
);
},
);
ui.separator();
// Table // Table
StripBuilder::new(ui).size(Size::remainder()).vertical(|mut strip| { render_strips(ui, available_width, 2, |ui, column| {
strip.strip(|builder| { if column == 0 {
builder.sizes(Size::remainder(), 2).horizontal(|mut strip| { if let Some(ctx) = left_ctx {
strip.cell(|ui| { extab_ui(ui, ctx, appearance, column);
extab_ui(ui, result.first_obj.as_ref(), selected_symbol, appearance, true); }
}); } else if column == 1 {
strip.cell(|ui| { if let Some(ctx) = right_ctx {
extab_ui(ui, result.second_obj.as_ref(), selected_symbol, appearance, false); extab_ui(ui, ctx, appearance, column);
}); }
}); }
});
}); });
ret
} }

View File

@@ -1,28 +1,28 @@
use std::default::Default; use std::default::Default;
use egui::{text::LayoutJob, Align, Label, Layout, Response, Sense, Vec2, Widget}; use egui::{text::LayoutJob, Id, Label, Response, RichText, Sense, Widget};
use egui_extras::{Column, TableBuilder, TableRow}; use egui_extras::TableRow;
use objdiff_core::{ use objdiff_core::{
arch::ObjArch,
diff::{ diff::{
display::{display_diff, DiffText, HighlightKind}, display::{display_diff, DiffText, HighlightKind},
ObjDiff, ObjInsDiff, ObjInsDiffKind, ObjDiff, ObjInsDiff, ObjInsDiffKind,
}, },
obj::{ObjInfo, ObjIns, ObjInsArg, ObjInsArgValue, ObjSection, ObjSymbol, SymbolRef}, obj::{
ObjInfo, ObjIns, ObjInsArg, ObjInsArgValue, ObjSection, ObjSectionKind, ObjSymbol,
SymbolRef,
},
}; };
use time::format_description; use time::format_description;
use crate::views::{ use crate::views::{
appearance::Appearance, appearance::Appearance,
symbol_diff::{match_color_for_symbol, DiffViewState, SymbolRefByName, View}, column_layout::{render_header, render_strips, render_table},
symbol_diff::{
match_color_for_symbol, symbol_list_ui, DiffViewAction, DiffViewNavigation, DiffViewState,
SymbolDiffContext, SymbolFilter, SymbolRefByName, SymbolViewState, View,
},
}; };
#[derive(Copy, Clone, Eq, PartialEq)]
enum ColumnId {
Left,
Right,
}
#[derive(Default)] #[derive(Default)]
pub struct FunctionViewState { pub struct FunctionViewState {
left_highlight: HighlightKind, left_highlight: HighlightKind,
@@ -30,16 +30,17 @@ pub struct FunctionViewState {
} }
impl FunctionViewState { impl FunctionViewState {
fn highlight(&self, column: ColumnId) -> &HighlightKind { pub fn highlight(&self, column: usize) -> &HighlightKind {
match column { match column {
ColumnId::Left => &self.left_highlight, 0 => &self.left_highlight,
ColumnId::Right => &self.right_highlight, 1 => &self.right_highlight,
_ => &HighlightKind::None,
} }
} }
fn set_highlight(&mut self, column: ColumnId, highlight: HighlightKind) { pub fn set_highlight(&mut self, column: usize, highlight: HighlightKind) {
match column { match column {
ColumnId::Left => { 0 => {
if highlight == self.left_highlight { if highlight == self.left_highlight {
if highlight == self.right_highlight { if highlight == self.right_highlight {
self.left_highlight = HighlightKind::None; self.left_highlight = HighlightKind::None;
@@ -51,7 +52,7 @@ impl FunctionViewState {
self.left_highlight = highlight; self.left_highlight = highlight;
} }
} }
ColumnId::Right => { 1 => {
if highlight == self.right_highlight { if highlight == self.right_highlight {
if highlight == self.left_highlight { if highlight == self.left_highlight {
self.left_highlight = HighlightKind::None; self.left_highlight = HighlightKind::None;
@@ -63,13 +64,19 @@ impl FunctionViewState {
self.right_highlight = highlight; self.right_highlight = highlight;
} }
} }
_ => {}
} }
} }
pub fn clear_highlight(&mut self) {
self.left_highlight = HighlightKind::None;
self.right_highlight = HighlightKind::None;
}
} }
fn ins_hover_ui( fn ins_hover_ui(
ui: &mut egui::Ui, ui: &mut egui::Ui,
arch: &dyn ObjArch, obj: &ObjInfo,
section: &ObjSection, section: &ObjSection,
ins: &ObjIns, ins: &ObjIns,
symbol: &ObjSymbol, symbol: &ObjSymbol,
@@ -112,10 +119,17 @@ fn ins_hover_ui(
} }
if let Some(reloc) = &ins.reloc { if let Some(reloc) = &ins.reloc {
ui.label(format!("Relocation type: {}", arch.display_reloc(reloc.flags))); ui.label(format!("Relocation type: {}", obj.arch.display_reloc(reloc.flags)));
ui.colored_label(appearance.highlight_color, format!("Name: {}", reloc.target.name)); ui.colored_label(appearance.highlight_color, format!("Name: {}", reloc.target.name));
if let Some(section) = &reloc.target_section { if let Some(orig_section_index) = reloc.target.orig_section_index {
ui.colored_label(appearance.highlight_color, format!("Section: {section}")); if let Some(section) =
obj.sections.iter().find(|s| s.orig_index == orig_section_index)
{
ui.colored_label(
appearance.highlight_color,
format!("Section: {}", section.name),
);
}
ui.colored_label( ui.colored_label(
appearance.highlight_color, appearance.highlight_color,
format!("Address: {:x}", reloc.target.address), format!("Address: {:x}", reloc.target.address),
@@ -124,9 +138,10 @@ fn ins_hover_ui(
appearance.highlight_color, appearance.highlight_color,
format!("Size: {:x}", reloc.target.size), format!("Size: {:x}", reloc.target.size),
); );
if let Some(s) = arch if let Some(s) = obj
.arch
.guess_data_type(ins) .guess_data_type(ins)
.and_then(|ty| arch.display_data_type(ty, &reloc.target.bytes)) .and_then(|ty| obj.arch.display_data_type(ty, &reloc.target.bytes))
{ {
ui.colored_label(appearance.highlight_color, s); ui.colored_label(appearance.highlight_color, s);
} }
@@ -218,17 +233,19 @@ fn find_symbol(obj: &ObjInfo, selected_symbol: &SymbolRefByName) -> Option<Symbo
None None
} }
#[allow(clippy::too_many_arguments)] #[must_use]
#[expect(clippy::too_many_arguments)]
fn diff_text_ui( fn diff_text_ui(
ui: &mut egui::Ui, ui: &mut egui::Ui,
text: DiffText<'_>, text: DiffText<'_>,
ins_diff: &ObjInsDiff, ins_diff: &ObjInsDiff,
appearance: &Appearance, appearance: &Appearance,
ins_view_state: &mut FunctionViewState, ins_view_state: &FunctionViewState,
column: ColumnId, column: usize,
space_width: f32, space_width: f32,
response_cb: impl Fn(Response) -> Response, response_cb: impl Fn(Response) -> Response,
) { ) -> Option<DiffViewAction> {
let mut ret = None;
let label_text; let label_text;
let mut base_color = match ins_diff.kind { let mut base_color = match ins_diff.kind {
ObjInsDiffKind::None | ObjInsDiffKind::OpMismatch | ObjInsDiffKind::ArgMismatch => { ObjInsDiffKind::None | ObjInsDiffKind::OpMismatch | ObjInsDiffKind::ArgMismatch => {
@@ -282,7 +299,7 @@ fn diff_text_ui(
} }
DiffText::Spacing(n) => { DiffText::Spacing(n) => {
ui.add_space(n as f32 * space_width); ui.add_space(n as f32 * space_width);
return; return ret;
} }
DiffText::Eol => { DiffText::Eol => {
label_text = "\n".to_string(); label_text = "\n".to_string();
@@ -299,22 +316,25 @@ fn diff_text_ui(
.ui(ui); .ui(ui);
response = response_cb(response); response = response_cb(response);
if response.clicked() { if response.clicked() {
ins_view_state.set_highlight(column, text.into()); ret = Some(DiffViewAction::SetDiffHighlight(column, text.into()));
} }
if len < pad_to { if len < pad_to {
ui.add_space((pad_to - len) as f32 * space_width); ui.add_space((pad_to - len) as f32 * space_width);
} }
ret
} }
#[must_use]
fn asm_row_ui( fn asm_row_ui(
ui: &mut egui::Ui, ui: &mut egui::Ui,
ins_diff: &ObjInsDiff, ins_diff: &ObjInsDiff,
symbol: &ObjSymbol, symbol: &ObjSymbol,
appearance: &Appearance, appearance: &Appearance,
ins_view_state: &mut FunctionViewState, ins_view_state: &FunctionViewState,
column: ColumnId, column: usize,
response_cb: impl Fn(Response) -> Response, response_cb: impl Fn(Response) -> Response,
) { ) -> Option<DiffViewAction> {
let mut ret = None;
ui.spacing_mut().item_spacing.x = 0.0; ui.spacing_mut().item_spacing.x = 0.0;
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
if ins_diff.kind != ObjInsDiffKind::None { if ins_diff.kind != ObjInsDiffKind::None {
@@ -322,7 +342,7 @@ fn asm_row_ui(
} }
let space_width = ui.fonts(|f| f.glyph_width(&appearance.code_font, ' ')); let space_width = ui.fonts(|f| f.glyph_width(&appearance.code_font, ' '));
display_diff(ins_diff, symbol.address, |text| { display_diff(ins_diff, symbol.address, |text| {
diff_text_ui( if let Some(action) = diff_text_ui(
ui, ui,
text, text,
ins_diff, ins_diff,
@@ -331,241 +351,476 @@ fn asm_row_ui(
column, column,
space_width, space_width,
&response_cb, &response_cb,
); ) {
ret = Some(action);
}
Ok::<_, ()>(()) Ok::<_, ()>(())
}) })
.unwrap(); .unwrap();
ret
} }
#[must_use]
fn asm_col_ui( fn asm_col_ui(
row: &mut TableRow<'_, '_>, row: &mut TableRow<'_, '_>,
obj: &(ObjInfo, ObjDiff), ctx: FunctionDiffContext<'_>,
symbol_ref: SymbolRef,
appearance: &Appearance, appearance: &Appearance,
ins_view_state: &mut FunctionViewState, ins_view_state: &FunctionViewState,
column: ColumnId, column: usize,
) { ) -> Option<DiffViewAction> {
let (section, symbol) = obj.0.section_symbol(symbol_ref); let mut ret = None;
let section = section.unwrap(); let symbol_ref = ctx.symbol_ref?;
let ins_diff = &obj.1.symbol_diff(symbol_ref).instructions[row.index()]; let (section, symbol) = ctx.obj.section_symbol(symbol_ref);
let section = section?;
let ins_diff = &ctx.diff.symbol_diff(symbol_ref).instructions[row.index()];
let response_cb = |response: Response| { let response_cb = |response: Response| {
if let Some(ins) = &ins_diff.ins { if let Some(ins) = &ins_diff.ins {
response.context_menu(|ui| ins_context_menu(ui, section, ins, symbol)); response.context_menu(|ui| ins_context_menu(ui, section, ins, symbol));
response.on_hover_ui_at_pointer(|ui| { response.on_hover_ui_at_pointer(|ui| {
ins_hover_ui(ui, obj.0.arch.as_ref(), section, ins, symbol, appearance) ins_hover_ui(ui, ctx.obj, section, ins, symbol, appearance)
}) })
} else { } else {
response response
} }
}; };
let (_, response) = row.col(|ui| { let (_, response) = row.col(|ui| {
asm_row_ui(ui, ins_diff, symbol, appearance, ins_view_state, column, response_cb); if let Some(action) =
asm_row_ui(ui, ins_diff, symbol, appearance, ins_view_state, column, response_cb)
{
ret = Some(action);
}
}); });
response_cb(response); response_cb(response);
ret
} }
fn empty_col_ui(row: &mut TableRow<'_, '_>) { #[must_use]
row.col(|ui| {
ui.label("");
});
}
fn asm_table_ui( fn asm_table_ui(
table: TableBuilder<'_>, ui: &mut egui::Ui,
left_obj: Option<&(ObjInfo, ObjDiff)>, available_width: f32,
right_obj: Option<&(ObjInfo, ObjDiff)>, left_ctx: Option<FunctionDiffContext<'_>>,
selected_symbol: &SymbolRefByName, right_ctx: Option<FunctionDiffContext<'_>>,
appearance: &Appearance, appearance: &Appearance,
ins_view_state: &mut FunctionViewState, ins_view_state: &FunctionViewState,
) -> Option<()> { symbol_state: &SymbolViewState,
let left_symbol = left_obj.and_then(|(obj, _)| find_symbol(obj, selected_symbol)); ) -> Option<DiffViewAction> {
let right_symbol = right_obj.and_then(|(obj, _)| find_symbol(obj, selected_symbol)); let mut ret = None;
let instructions_len = match (left_symbol, right_symbol) { let left_len = left_ctx.and_then(|ctx| {
(Some(left_symbol_ref), Some(right_symbol_ref)) => { ctx.symbol_ref.map(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).instructions.len())
let left_len = left_obj.unwrap().1.symbol_diff(left_symbol_ref).instructions.len(); });
let right_len = right_obj.unwrap().1.symbol_diff(right_symbol_ref).instructions.len(); let right_len = right_ctx.and_then(|ctx| {
debug_assert_eq!(left_len, right_len); ctx.symbol_ref.map(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).instructions.len())
});
let instructions_len = match (left_len, right_len) {
(Some(left_len), Some(right_len)) => {
if left_len != right_len {
ui.label("Instruction count mismatch");
return None;
}
left_len left_len
} }
(Some(left_symbol_ref), None) => { (Some(left_len), None) => left_len,
left_obj.unwrap().1.symbol_diff(left_symbol_ref).instructions.len() (None, Some(right_len)) => right_len,
(None, None) => {
ui.label("No symbol selected");
return None;
} }
(None, Some(right_symbol_ref)) => {
right_obj.unwrap().1.symbol_diff(right_symbol_ref).instructions.len()
}
(None, None) => return None,
}; };
table.body(|body| { if left_len.is_some() && right_len.is_some() {
body.rows(appearance.code_font.size, instructions_len, |mut row| { // Joint view
if let (Some(left_obj), Some(left_symbol_ref)) = (left_obj, left_symbol) { render_table(
asm_col_ui( ui,
&mut row, available_width,
left_obj, 2,
left_symbol_ref, appearance.code_font.size,
appearance, instructions_len,
ins_view_state, |row, column| {
ColumnId::Left, if column == 0 {
); if let Some(ctx) = left_ctx {
} else { if let Some(action) =
empty_col_ui(&mut row); asm_col_ui(row, ctx, appearance, ins_view_state, column)
} {
if let (Some(right_obj), Some(right_symbol_ref)) = (right_obj, right_symbol) { ret = Some(action);
asm_col_ui( }
&mut row, }
right_obj, } else if column == 1 {
right_symbol_ref, if let Some(ctx) = right_ctx {
appearance, if let Some(action) =
ins_view_state, asm_col_ui(row, ctx, appearance, ins_view_state, column)
ColumnId::Right, {
); ret = Some(action);
} else { }
empty_col_ui(&mut row); }
if row.response().clicked() {
ret = Some(DiffViewAction::ClearDiffHighlight);
}
}
},
);
} else {
// Split view, one side is the symbol list
render_strips(ui, available_width, 2, |ui, column| {
if column == 0 {
if let Some(ctx) = left_ctx {
if ctx.has_symbol() {
render_table(
ui,
available_width / 2.0,
1,
appearance.code_font.size,
instructions_len,
|row, column| {
if let Some(action) =
asm_col_ui(row, ctx, appearance, ins_view_state, column)
{
ret = Some(action);
}
if row.response().clicked() {
ret = Some(DiffViewAction::ClearDiffHighlight);
}
},
);
} else if let Some((right_ctx, right_symbol_ref)) =
right_ctx.and_then(|ctx| ctx.symbol_ref.map(|symbol_ref| (ctx, symbol_ref)))
{
if let Some(action) = symbol_list_ui(
ui,
SymbolDiffContext { obj: ctx.obj, diff: ctx.diff },
None,
symbol_state,
SymbolFilter::Mapping(right_symbol_ref),
appearance,
column,
) {
match action {
DiffViewAction::Navigate(DiffViewNavigation {
left_symbol: Some(left_symbol_ref),
..
}) => {
let (right_section, right_symbol) =
right_ctx.obj.section_symbol(right_symbol_ref);
ret = Some(DiffViewAction::SetMapping(
match right_section.map(|s| s.kind) {
Some(ObjSectionKind::Code) => View::FunctionDiff,
_ => View::SymbolDiff,
},
left_symbol_ref,
SymbolRefByName::new(right_symbol, right_section),
));
}
DiffViewAction::SetSymbolHighlight(_, _) => {
// Ignore
}
_ => {
ret = Some(action);
}
}
}
}
} else {
ui.label("No left object");
}
} else if column == 1 {
if let Some(ctx) = right_ctx {
if ctx.has_symbol() {
render_table(
ui,
available_width / 2.0,
1,
appearance.code_font.size,
instructions_len,
|row, column| {
if let Some(action) =
asm_col_ui(row, ctx, appearance, ins_view_state, column)
{
ret = Some(action);
}
if row.response().clicked() {
ret = Some(DiffViewAction::ClearDiffHighlight);
}
},
);
} else if let Some((left_ctx, left_symbol_ref)) =
left_ctx.and_then(|ctx| ctx.symbol_ref.map(|symbol_ref| (ctx, symbol_ref)))
{
if let Some(action) = symbol_list_ui(
ui,
SymbolDiffContext { obj: ctx.obj, diff: ctx.diff },
None,
symbol_state,
SymbolFilter::Mapping(left_symbol_ref),
appearance,
column,
) {
match action {
DiffViewAction::Navigate(DiffViewNavigation {
right_symbol: Some(right_symbol_ref),
..
}) => {
let (left_section, left_symbol) =
left_ctx.obj.section_symbol(left_symbol_ref);
ret = Some(DiffViewAction::SetMapping(
match left_section.map(|s| s.kind) {
Some(ObjSectionKind::Code) => View::FunctionDiff,
_ => View::SymbolDiff,
},
SymbolRefByName::new(left_symbol, left_section),
right_symbol_ref,
));
}
DiffViewAction::SetSymbolHighlight(_, _) => {
// Ignore
}
_ => {
ret = Some(action);
}
}
}
}
} else {
ui.label("No right object");
}
} }
}); });
}); }
Some(()) ret
} }
pub fn function_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &Appearance) { #[derive(Clone, Copy)]
let (Some(result), Some(selected_symbol)) = (&state.build, &state.symbol_state.selected_symbol) pub struct FunctionDiffContext<'a> {
else { pub obj: &'a ObjInfo,
return; pub diff: &'a ObjDiff,
pub symbol_ref: Option<SymbolRef>,
}
impl<'a> FunctionDiffContext<'a> {
pub fn new(
obj: Option<&'a (ObjInfo, ObjDiff)>,
selected_symbol: Option<&SymbolRefByName>,
) -> Option<Self> {
obj.map(|(obj, diff)| Self {
obj,
diff,
symbol_ref: selected_symbol.and_then(|s| find_symbol(obj, s)),
})
}
#[inline]
pub fn has_symbol(&self) -> bool { self.symbol_ref.is_some() }
}
#[must_use]
pub fn function_diff_ui(
ui: &mut egui::Ui,
state: &DiffViewState,
appearance: &Appearance,
) -> Option<DiffViewAction> {
let mut ret = None;
let Some(result) = &state.build else {
return ret;
}; };
let mut left_ctx = FunctionDiffContext::new(
result.first_obj.as_ref(),
state.symbol_state.left_symbol.as_ref(),
);
let mut right_ctx = FunctionDiffContext::new(
result.second_obj.as_ref(),
state.symbol_state.right_symbol.as_ref(),
);
// If one side is missing a symbol, but the diff process found a match, use that symbol
let left_diff_symbol = left_ctx.and_then(|ctx| {
ctx.symbol_ref.and_then(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).target_symbol)
});
let right_diff_symbol = right_ctx.and_then(|ctx| {
ctx.symbol_ref.and_then(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).target_symbol)
});
if left_diff_symbol.is_some() && right_ctx.map_or(false, |ctx| !ctx.has_symbol()) {
let (right_section, right_symbol) =
right_ctx.unwrap().obj.section_symbol(left_diff_symbol.unwrap());
let symbol_ref = SymbolRefByName::new(right_symbol, right_section);
right_ctx = FunctionDiffContext::new(result.second_obj.as_ref(), Some(&symbol_ref));
ret = Some(DiffViewAction::Navigate(DiffViewNavigation {
view: Some(View::FunctionDiff),
left_symbol: state.symbol_state.left_symbol.clone(),
right_symbol: Some(symbol_ref),
}));
} else if right_diff_symbol.is_some() && left_ctx.map_or(false, |ctx| !ctx.has_symbol()) {
let (left_section, left_symbol) =
left_ctx.unwrap().obj.section_symbol(right_diff_symbol.unwrap());
let symbol_ref = SymbolRefByName::new(left_symbol, left_section);
left_ctx = FunctionDiffContext::new(result.first_obj.as_ref(), Some(&symbol_ref));
ret = Some(DiffViewAction::Navigate(DiffViewNavigation {
view: Some(View::FunctionDiff),
left_symbol: Some(symbol_ref),
right_symbol: state.symbol_state.right_symbol.clone(),
}));
}
// If both sides are missing a symbol, switch to symbol diff view
if right_ctx.map_or(false, |ctx| !ctx.has_symbol())
&& left_ctx.map_or(false, |ctx| !ctx.has_symbol())
{
return Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
}
// Header // Header
let available_width = ui.available_width(); let available_width = ui.available_width();
let column_width = available_width / 2.0; render_header(ui, available_width, 2, |ui, column| {
ui.allocate_ui_with_layout( if column == 0 {
Vec2 { x: available_width, y: 100.0 },
Layout::left_to_right(Align::Min),
|ui| {
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate);
// Left column // Left column
ui.allocate_ui_with_layout( ui.horizontal(|ui| {
Vec2 { x: column_width, y: 100.0 }, if ui.button("⏴ Back").clicked() {
Layout::top_down(Align::Min), ret = Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
|ui| { }
ui.set_width(column_width); ui.separator();
if ui
ui.horizontal(|ui| { .add_enabled(
if ui.button("⏴ Back").clicked() { !state.scratch_running
state.current_view = View::SymbolDiff; && state.scratch_available
} && left_ctx.map_or(false, |ctx| ctx.has_symbol()),
ui.separator(); egui::Button::new("📲 decomp.me"),
if ui )
.add_enabled( .on_hover_text_at_pointer("Create a new scratch on decomp.me (beta)")
!state.scratch_running && state.scratch_available, .on_disabled_hover_text("Scratch configuration missing")
egui::Button::new("📲 decomp.me"), .clicked()
) {
.on_hover_text_at_pointer("Create a new scratch on decomp.me (beta)") if let Some((_section, symbol)) = left_ctx.and_then(|ctx| {
.on_disabled_hover_text("Scratch configuration missing") ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref))
.clicked() }) {
{ ret = Some(DiffViewAction::CreateScratch(symbol.name.clone()));
state.queue_scratch = true; }
} }
}); });
let name = selected_symbol
.demangled_symbol_name
.as_deref()
.unwrap_or(&selected_symbol.symbol_name);
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.colored_label(appearance.highlight_color, name);
ui.label("Diff target:");
});
},
);
if let Some((_section, symbol)) = left_ctx
.and_then(|ctx| ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref)))
{
let name = symbol.demangled_name.as_deref().unwrap_or(&symbol.name);
ui.label(
RichText::new(name)
.font(appearance.code_font.clone())
.color(appearance.highlight_color),
);
if right_ctx.map_or(false, |m| m.has_symbol())
&& ui
.button("Change target")
.on_hover_text_at_pointer("Choose a different symbol to use as the target")
.clicked()
{
if let Some(symbol_ref) = state.symbol_state.right_symbol.as_ref() {
ret = Some(DiffViewAction::SelectingLeft(symbol_ref.clone()));
}
}
} else {
ui.label(
RichText::new("Missing")
.font(appearance.code_font.clone())
.color(appearance.replace_color),
);
ui.label(
RichText::new("Choose target symbol")
.font(appearance.code_font.clone())
.color(appearance.highlight_color),
);
}
} else if column == 1 {
// Right column // Right column
ui.allocate_ui_with_layout( ui.horizontal(|ui| {
Vec2 { x: column_width, y: 100.0 }, if ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() {
Layout::top_down(Align::Min), ret = Some(DiffViewAction::Build);
|ui| { }
ui.set_width(column_width); ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
if state.build_running {
ui.colored_label(appearance.replace_color, "Building…");
} else {
ui.label("Last built:");
let format = format_description::parse("[hour]:[minute]:[second]").unwrap();
ui.label(
result.time.to_offset(appearance.utc_offset).format(&format).unwrap(),
);
}
});
ui.separator();
if ui
.add_enabled(state.source_path_available, egui::Button::new("🖹 Source file"))
.on_hover_text_at_pointer("Open the source file in the default editor")
.on_disabled_hover_text("Source file metadata missing")
.clicked()
{
ret = Some(DiffViewAction::OpenSourcePath);
}
});
ui.horizontal(|ui| { if let Some(((_section, symbol), symbol_diff)) = right_ctx.and_then(|ctx| {
if ui ctx.symbol_ref.map(|symbol_ref| {
.add_enabled(!state.build_running, egui::Button::new("Build")) (ctx.obj.section_symbol(symbol_ref), ctx.diff.symbol_diff(symbol_ref))
.clicked() })
{ }) {
state.queue_build = true; let name = symbol.demangled_name.as_deref().unwrap_or(&symbol.name);
} ui.label(
ui.scope(|ui| { RichText::new(name)
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); .font(appearance.code_font.clone())
if state.build_running { .color(appearance.highlight_color),
ui.colored_label(appearance.replace_color, "Building…"); );
} else { ui.horizontal(|ui| {
ui.label("Last built:"); if let Some(match_percent) = symbol_diff.match_percent {
let format = ui.label(
format_description::parse("[hour]:[minute]:[second]").unwrap(); RichText::new(format!("{:.0}%", match_percent.floor()))
ui.label( .font(appearance.code_font.clone())
result .color(match_color_for_symbol(match_percent, appearance)),
.time );
.to_offset(appearance.utc_offset) }
.format(&format) if left_ctx.map_or(false, |m| m.has_symbol()) {
.unwrap(),
);
}
});
ui.separator(); ui.separator();
if ui if ui
.add_enabled( .button("Change base")
state.source_path_available, .on_hover_text_at_pointer(
egui::Button::new("🖹 Source file"), "Choose a different symbol to use as the base",
) )
.on_hover_text_at_pointer("Open the source file in the default editor")
.on_disabled_hover_text("Source file metadata missing")
.clicked() .clicked()
{ {
state.queue_open_source_path = true; if let Some(symbol_ref) = state.symbol_state.left_symbol.as_ref() {
ret = Some(DiffViewAction::SelectingRight(symbol_ref.clone()));
}
} }
}); }
});
ui.scope(|ui| { } else {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); ui.label(
if let Some(match_percent) = result RichText::new("Missing")
.second_obj .font(appearance.code_font.clone())
.as_ref() .color(appearance.replace_color),
.and_then(|(obj, diff)| { );
find_symbol(obj, selected_symbol).map(|sref| { ui.label(
&diff.sections[sref.section_idx].symbols[sref.symbol_idx] RichText::new("Choose base symbol")
}) .font(appearance.code_font.clone())
}) .color(appearance.highlight_color),
.and_then(|symbol| symbol.match_percent) );
{ }
ui.colored_label( }
match_color_for_symbol(match_percent, appearance), });
format!("{match_percent:.0}%"),
);
} else {
ui.colored_label(appearance.replace_color, "Missing");
}
ui.label("Diff base:");
});
},
);
},
);
ui.separator();
// Table // Table
ui.style_mut().interaction.selectable_labels = false; let id = Id::new(state.symbol_state.left_symbol.as_ref().map(|s| s.symbol_name.as_str()))
let available_height = ui.available_height(); .with(state.symbol_state.right_symbol.as_ref().map(|s| s.symbol_name.as_str()));
let table = TableBuilder::new(ui) if let Some(action) = ui
.striped(false) .push_id(id, |ui| {
.cell_layout(Layout::left_to_right(Align::Min)) asm_table_ui(
.columns(Column::exact(column_width).clip(true), 2) ui,
.resizable(false) available_width,
.auto_shrink([false, false]) left_ctx,
.min_scrolled_height(available_height); right_ctx,
asm_table_ui( appearance,
table, &state.function_state,
result.first_obj.as_ref(), &state.symbol_state,
result.second_obj.as_ref(), )
selected_symbol, })
appearance, .inner
&mut state.function_state, {
); ret = Some(action);
}
ret
} }

View File

@@ -1,6 +1,7 @@
use egui::{text::LayoutJob, Color32, FontId, TextFormat}; use egui::{text::LayoutJob, Color32, FontId, TextFormat};
pub(crate) mod appearance; pub(crate) mod appearance;
pub(crate) mod column_layout;
pub(crate) mod config; pub(crate) mod config;
pub(crate) mod data_diff; pub(crate) mod data_diff;
pub(crate) mod debug; pub(crate) mod debug;

File diff suppressed because it is too large Load Diff