Compare commits

..

245 Commits

Author SHA1 Message Date
LagoLunatic
221c1a8cda Merge 0b7afa9f1e into 7aa878b48e 2024-12-01 22:33:58 -07:00
7aa878b48e Update all dependencies & clippy fixes 2024-12-01 22:22:35 -07:00
LagoLunatic
0b7afa9f1e Remove trailing newline when displaying decoded rlwinm info 2024-12-01 20:34:48 -05:00
LagoLunatic
5841d4db8a Display decoded rlwinm info to hover tooltip 2024-12-01 20:27:56 -05:00
LagoLunatic
7a360d4a99 Update .gitignore 2024-12-01 20:26:17 -05:00
LagoLunatic
40c97a1bc3 Fix missing dependency feature for objdiff-gui 2024-12-01 20:26:17 -05:00
a119d9a6dd Add scratch preset_id field for decomp.me
Resolves #133
2024-11-07 09:27:13 -07:00
robojumper
ebf653816a Combine nested otherwise empty directories in objects view (#137) 2024-11-07 08:21:39 -07:00
424434edd6 Experimental ARM64 support
Based on yaxpeax-arm, but with a heavy dose of
custom code to work around its limitations.

Please report any issues or unhandled relocations.
2024-10-31 17:39:12 -06:00
7f14b684bf Ignore PlainText segments when diffing 2024-10-31 17:27:27 -06:00
c5da7f7dd5 Show diff color when symbols differ 2024-10-31 17:26:59 -06:00
2fd655850a Ignore Absolute relocations and log warning 2024-10-31 17:24:49 -06:00
79bd7317c1 Match BranchDest->Reloc with relaxed relocation diffs 2024-10-31 17:24:33 -06:00
21f8f2407c Relax symbol comparison logic
The Ghidra delinker plugin emits functions with type STT_OBJECT,
rather than STT_FUNC. The current logic was preventing these from
being compared based on their symbol type. Relax this condition
for now.
2024-10-29 22:46:02 -06:00
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
a5d9d8282e Update all dependencies 2024-10-03 22:00:43 -06:00
Amber Brault
3287a0f65c Bump cwextab again to 1.0.2 (#114)
* Bump cwextab

* Updated cwextab to not error on null actions

* Bump cwextab again
2024-10-03 01:12:37 -06:00
Amber Brault
fab9c62dfb Bump cwextab (#113)
* Bump cwextab

* Updated cwextab to not error on null actions
2024-10-01 23:20:09 -06:00
08cd768260 Add total_units, complete_units to progress report 2024-09-30 21:41:57 -06:00
8acaaf528c Version v2.2.0 2024-09-29 12:26:41 -06:00
6e881a74e1 Remove armv7-unknown-linux-musleabi build 2024-09-29 11:57:13 -06:00
cc1bc44e69 Use mimalloc when targeting musl 2024-09-29 11:52:04 -06:00
c7b85518ab Rework jobs view & error handling improvements
Job status is now shown in the top menu bar,
with a new Jobs window that can be toggled.

Build and diff errors are now handled more
gracefully.

Fixes #40
2024-09-28 12:14:20 -06:00
bb039a1445 Add "Open source file" option
Available when right-clicking an object in
the object list or when viewing an object

Resolves #99
2024-09-28 11:50:56 -06:00
8fc142d316 Debounce loaded object modification check
Before, this was running 2 fs::metadata
calls every frame. We don't need to do it
nearly that often, so now it only checks
once every 500ms.

This required refactoring AppConfig into
a separate AppState that holds transient
runtime state along with the loaded
AppConfig.
2024-09-28 10:55:22 -06:00
b0123b3f83 Improve build log message when command doesn't exist
Before, it didn't include the actual command
that was attempted to run.
2024-09-28 10:55:09 -06:00
2ec17aee9b Improve config read/write performance
We were accidentally using unbuffered readers
and writers before, leading to long pauses on
the main thread on slow filesystems. (e.g.
FUSE or WSL)
2024-09-28 10:54:54 -06:00
ec9731e1e5 Set app_id in eframe NativeOptions
Fixes missing WM_CLASS on Wayland
2024-09-28 10:53:58 -06:00
OndrikB
a06382c27e Disambiguate dummy symbols (#107)
* Disambiguate dummy symbols

* Small formatting improvement

* Put HashMap logic into symbol creation
2024-09-27 00:33:36 -06:00
e013638c5a clippy fixes 2024-09-27 00:30:30 -06:00
70ab82f1f7 gui: Highlight registers in columns separately
This matches the behavior of decomp.me and the
CLI.

Resolves #71
2024-09-27 00:27:36 -06:00
c5896689cf Use ppc750cl Opcode::from 2024-09-27 00:12:21 -06:00
67719dd93e report: Exclude "hidden" functions
Fixes #111
2024-09-27 00:12:21 -06:00
258e141017 Upgrade all dependencies 2024-09-27 00:12:16 -06:00
dbdda55065 Add Report::split
A hack for supporting games that build
all versions at once.
2024-09-26 23:47:03 -06:00
Steven Casper
a43320af1f PPC: Guess reloc data type based on the instruction. (#108)
* Guess reloc data type based on the instruction.

Adds an entry to the reloc tooltip to show the inferred data type
and value.

* Fix clippy warning

* Match on Opcode rather than mnemonic string
2024-09-25 23:45:37 -06:00
Amber Brault
35bbd40f5d Actually update extab stuff (#110)
* Update cwextab

* Update

* Update ppc.rs

* Make fmt shut up
2024-09-24 09:16:14 -06:00
Amber Brault
c1cb4b0b19 Update cwextab (#109) 2024-09-23 21:24:33 -06:00
2379853faa Remove unused imports 2024-09-10 23:29:22 -06:00
5e1aff180f Remove vergen / GIT_COMMIT_SHA handling 2024-09-10 23:22:40 -06:00
3846a7d315 Version v2.0.0 2024-09-09 20:18:56 -06:00
dcf209aac5 Cleanup & move extab code into ppc arch 2024-09-09 19:43:10 -06:00
c7e6394628 Try to resolve deleting autoupdate tmp dir 2024-09-09 19:42:01 -06:00
235dc7f517 Use released ppc750cl & update README.md 2024-09-09 19:41:29 -06:00
Robin Avery
199c07e975 Add cargo install instructions to README (#105) 2024-09-09 19:38:06 -06:00
56a5a61825 Updates to CI workflow & README.md 2024-09-09 19:34:50 -06:00
3d2236de82 Use workspace keys in Cargo.toml 2024-09-09 19:32:22 -06:00
bcc5871cd8 Update all dependencies 2024-09-09 19:26:46 -06:00
Robin Lambertz
7d0d7df54c Add 32-bit windows objdiff-cli build (#102)
* Revert "Add 32-bit windows builds (#101)"

This reverts commit bc687173c0.

* Add 32-bit objdiff-cli build
2024-09-06 19:25:18 -06:00
0221a2d54d clippy fix 2024-09-05 17:52:43 -06:00
Robin Lambertz
bc687173c0 Add 32-bit windows builds (#101) 2024-09-05 17:50:37 -06:00
e1ae369d17 CI: Fix Cargo.toml version check 2024-09-04 23:51:42 -06:00
ce05d6d6c0 Version v2.0.0-beta.6 2024-09-04 23:36:41 -06:00
c16a926d9b objdiff-cli: Build static binary & for more arches 2024-09-04 23:33:52 -06:00
Robin Lambertz
a32d99923c Coff line number (#100)
* Update object to 0.36

* Add COFF line number support
2024-09-04 18:36:09 -06:00
68606dfdcb Add config.schema.json & update README.md 2024-09-03 20:48:45 -06:00
b4650b660a Hide auto-generated objects in object list
With filter option to display them,
if desired. decomp-toolkit will
start writing auto-generated objects
in objdiff.json for reporting
purposes, so this maintains the
existing behavior.
2024-09-03 18:59:07 -06:00
195379968c Support for progress categories & linked stats 2024-09-03 00:59:15 -06:00
Aetias
3bd8aaee41 Bump unarm to 1.5.0 (#98) 2024-08-25 20:57:45 -06:00
1f4175dc21 Overall wasm refactoring & improvements 2024-08-21 19:48:58 -06:00
0fccae1049 Add experimental wasm bindings
Published to npm as objdiff-wasm
2024-08-20 21:40:32 -06:00
8250d26b77 Support R_MIPS_LITERAL, R_MIPS15_S3 relocations
Resolves #92
Resolves #95
2024-08-18 22:05:16 -06:00
fd555a6e0f Fix reading little-endian .line section 2024-08-18 21:57:53 -06:00
3710b6a91e Try even harder to recover from protoc missing 2024-08-18 14:01:49 -06:00
faebddbc5e More updates to report types 2024-08-18 13:42:41 -06:00
a733a950a3 Avoid requiring protoc unless protos change 2024-08-18 13:40:49 -06:00
cad9b70632 Support protobuf format for reports
This migrates to using protobuf to
define the "report" and "changes"
formats in objdiff-cli.

The JSON output now uses the Proto3
"JSON Mapping", which is slightly
incompatible with the existing JSON
format. Mainly, 64-bit numbers are
represented as strings, and addresses
are decimal strings instead of hex.

However, the older JSON format is
still accepted by "report changes"
to ease migration.
2024-08-16 00:52:24 -06:00
cf937b0be9 Guard against symbols larger than section 2024-08-11 16:54:24 -06:00
23b6d33a98 Fix botched find/replace 2024-08-11 16:53:35 -06:00
f17ee83622 Version v2.0.0-beta.3 2024-08-11 16:02:36 -06:00
615ec4c50a mips: Support R_MIPS_PC16 relocations 2024-08-11 16:00:10 -06:00
2cc10b0d06 cargo +nightly fmt 2024-08-11 14:29:58 -06:00
8091941448 Version v2.0.0-beta.2 2024-08-11 14:27:47 -06:00
de74dfdba7 Add dummy symbols to empty sections
Allows diffing sections without symbols

Resolves #87
2024-08-11 14:27:27 -06:00
177bd5e895 Fix branch immediates missing diff colors
Resolves #86
2024-08-11 14:01:53 -06:00
e1ccee1e73 Improve data section diffing more
This re-implements the older algorithm
used for data and BSS section match
percentages. We perform both and
choose the highest match percent
between the two options.

Resolves #84, #85
2024-08-11 13:53:52 -06:00
952b6a63c3 Better graphics backend fallback
This attempts the following in order:
- wgpu with user-selected backend
- wgpu with automatic backend
- glow (fallback OpenGL backend)

This should eliminate most issues
where objdiff fails to launch.
2024-08-11 13:33:10 -06:00
Steven Casper
09cc9952df Support R_MIPS_GPREL16 relocations correctly (#88)
* Support R_MIPS_GPREL16 relocations correctly

symbols defined in the same file require adding a
special ri_gp_value from the .reginfo section to
their relocation calculations.

* Run nightly rustfmt

* Prevent potential panic when slicing .reginfo
2024-08-08 20:20:41 -06:00
fc598af329 Version 2.0.0-beta.1 2024-07-21 23:03:15 -06:00
871407622d Use regex for symbol search
Also fixes case insensitivity and
properly searches the .comm section

Fixes #80
2024-07-21 23:01:58 -06:00
e3fff7b0dc Improve data section diff logic
More accurate in more cases

Fixes #81
2024-07-21 22:52:46 -06:00
Amber Brault
75b0e7d9e5 Exception table diff view (#82)
* Basic integration

* Implement basic right click option

Needs lotsa work

* nothing to worry about

* Convert extab diff to separate view

* Make clippy and fmt shut up

* Make clippy fmt shut up for real this time

* Print extab/extabindex symbol names in extab view

* I hate fmt

* Basic integration

* Implement basic right click option

Needs lotsa work

* nothing to worry about

* Convert extab diff to separate view

* Make clippy and fmt shut up

* Make clippy fmt shut up for real this time

* Print extab/extabindex symbol names in extab view

* I hate fmt

* Fix scroll position not being maintained from extab view

* Silly me

* Add rlwinm decoder window

* Remove extra files

* Create Cargo.lock

* Show extab symbol names in hover window

* Appease fmt

* Update symbol_diff.rs

* Update symbol_diff.rs

* Get extab symbol from extabindex relocations instead

* Update Cargo.lock

* Update Cargo.lock
2024-07-21 22:25:54 -06:00
Amber Brault
9f71ce9fea Add rlwinm decoder window (#83)
* Add rlwinm decoder window

* Remove extra files

* Create Cargo.lock

* Make fmt happy

* Update Cargo.lock

* Update Cargo.lock

* Update Cargo.lock
2024-07-21 17:56:46 -06:00
Aetias
d9fb48853e Options for ARM disassembly style (#78) 2024-07-14 16:00:57 -06:00
233839346a Version v2.0.0-alpha.5 2024-06-20 20:31:31 -06:00
95615c2ec5 Improve MIPS ABI auto-detection 2024-06-20 19:57:18 -06:00
Aetias
97981160f4 ARMv4T (GBA) and ARMv6K (3DS) support (#75)
* Initial ARM support

* Disassemble const pool reloc

* Disasm ARM/Thumb/data based on mapping symbols

* Fallback to mapping symbol `$a`

* Support multiple DWARF sequences

* Update line info

* Rework DWARF line info parsing

- Properly handles multiple sections
  in DWARF 1
- line_info moved into ObjSection
- DWARF 2 parser no longer errors with
  no .text section
- Both parsers properly skip empty
  sections

* Simplify line_info (no Option)

* Get line info from section; output formatted ins string

* Unwrap code section in `arm.rs`

* Handle reloc `R_ARM_SBREL32`

* Update ARM disassembler

* Update README.md

* Format

* Revert "Update README.md"

This reverts commit 8bbfcc6f45.

* Update README.md

* Detect ARM version; support ARMv4T and v6K

* Combobox to force ARM version

* Clear LSB in ARM symbol addresses

* Support big-endian ARM ELF files

* Bump `unarm`, `arm-attr`

* Handle ARM implicit addends

* Update README.md

* Explicitly handle all ARM argument types

* Format

* Display more ARM relocs

* Mask LSB on ARM code symbols only

* Read ARM implicit addends

* Format

---------

Co-authored-by: Luke Street <luke.street@encounterpc.com>
2024-06-20 18:36:25 -06:00
Aetias
1fd901a863 Option to combine data sections (#76)
Co-authored-by: Luke Street <luke.street@encounterpc.com>
2024-06-18 22:05:24 -06:00
759d55994a Fix clippy warning 2024-06-18 21:49:19 -06:00
9710ccc38a Add graphics backend configuration
Hopefully #74, #73, #56
2024-06-05 18:01:03 -06:00
79cd460333 Update notify-rs to fix WSL crash
Fixes #66
2024-06-04 17:13:54 -06:00
Aetias
a5a6a3928e Fix read error on objects with no .text section (#67)
* Fix read error on objects with no .text section

* Fix read error on DWARF 1.1 objects

* Revert DWARF 1 changes

---------

Co-authored-by: Luke Street <luke@street.dev>
2024-06-03 19:47:38 -06:00
fc54e93681 API updates for ARM backend 2024-06-03 19:37:48 -06:00
c9b11db2fa Update README.md 2024-06-03 19:09:35 -06:00
Aetias
b991960080 ARMv5TE (DS) support (#68)
* Initial ARM support

* Disassemble const pool reloc

* Disasm ARM/Thumb/data based on mapping symbols

* Fallback to mapping symbol `$a`

* Support multiple DWARF sequences

* Update line info

* Rework DWARF line info parsing

- Properly handles multiple sections
  in DWARF 1
- line_info moved into ObjSection
- DWARF 2 parser no longer errors with
  no .text section
- Both parsers properly skip empty
  sections

* Simplify line_info (no Option)

* Get line info from section; output formatted ins string

* Unwrap code section in `arm.rs`

* Handle reloc `R_ARM_SBREL32`

* Update ARM disassembler

* Update README.md

* Format

* Revert "Update README.md"

This reverts commit 8bbfcc6f45.

* Update README.md

---------

Co-authored-by: Luke Street <luke.street@encounterpc.com>
2024-06-03 19:08:49 -06:00
425dc8546b More descriptive message for build failure
Resolves #64
2024-06-03 19:06:19 -06:00
9e04357d9f Use solid scrollbar in egui
Resolves #69
2024-06-03 19:03:33 -06:00
6037c12ad0 Disable lto to workaround crash
See #66
2024-06-03 18:58:25 -06:00
b15f643713 Bump version to 2.0.0-alpha.3 2024-06-03 18:54:46 -06:00
3f82c1a50f objdiff-core API adjustments
- Allows using process_code without
  constructing an ObjInfo
- Allows creating an arch without
  having to provide an object

Used in decomp-toolkit
2024-06-03 18:52:32 -06:00
0ea6242669 Bump rabbitizer version (fixes crash) 2024-06-03 18:50:22 -06:00
0c20a0d9cd Update README.md 2024-05-21 18:12:58 -06:00
f30b3cfae2 Default "Space between args" -> true 2024-05-21 18:09:46 -06:00
9e57a66a05 Auto-detect MIPS ABI/category & add config
Under Diff Options -> Arch Settings, one
can override the ABI/instruction category
2024-05-21 18:06:14 -06:00
e254af5acf Support bss and text section diffing
Display section diff % in symbols view
2024-05-21 12:02:00 -06:00
Robin Avery
320efcb8cb objdiff-cli report: Support data sections (#49)
* objdiff-cli report: Support data sections

* Minor fixes for section match %

---------

Co-authored-by: Luke Street <luke.street@encounterpc.com>
2024-05-21 12:01:10 -06:00
7148b51fe0 x86: Handle IMAGE_REL_I386_REL32 LabelAddress
Resolves #57
2024-05-21 10:16:45 -06:00
dc0c170db9 Add .obj to object select filter
Resolves #54
2024-05-21 10:12:40 -06:00
Aetias
31e9c14681 Allow None section when parsing line info
Fixes an error upon ending the last DWARF sequence
2024-05-21 10:10:16 -06:00
94f1f07b00 Bump to 2.0.0-alpha.1 & fix version checks 2024-05-21 09:55:45 -06:00
Aetias
f5b5a612fc Display correct line numbers for multiple .text sections (#63)
* Support multiple DWARF sequences

* Rework DWARF line info parsing

- Properly handles multiple sections
  in DWARF 1
- line_info moved into ObjSection
- DWARF 2 parser no longer errors with
  no .text section
- Both parsers properly skip empty
  sections

* Simplify line_info (no Option)

---------

Co-authored-by: Luke Street <luke.street@encounterpc.com>
2024-05-21 09:55:39 -06:00
22a24f37f5 Diff data symbols & improve symbol match logic 2024-05-20 23:53:37 -06:00
Aetias
854dc9e4f5 Use base arch when processing base code (#62) 2024-05-20 17:38:58 -06:00
5bfaaaaf65 Instruction hover / context menu improvements 2024-05-20 17:38:20 -06:00
cadmic
8b36fa4fc6 Fix size of .note.split section (#61)
* Fix size of .note.split section

* clippy fix

---------

Co-authored-by: Luke Street <luke@street.dev>
2024-05-16 18:29:24 -06:00
660e6c879e Update README.md 2024-05-15 19:01:55 -06:00
Aetias
db726a68a6 Strip distro root prefix (#58) 2024-05-15 18:56:08 -06:00
Aetias
b457453639 Add custom make args (#59) 2024-05-15 18:53:14 -06:00
3e5008524e cargo fmt & cargo deny fix 2024-04-30 20:45:45 -06:00
2c46286aff Update all dependencies & use ppc750cl InsIter 2024-04-30 20:06:04 -06:00
106652ae7d Fix PPC branch display; update README.md 2024-03-22 23:06:41 -06:00
30d14870ef Update ppc750cl, add Itanium demangler & cleanup 2024-03-21 21:36:50 -06:00
e7991cb28d cargo fmt 2024-03-18 22:56:57 -06:00
4dfc28fc68 Diff cleanup & fixes 2024-03-18 22:56:13 -06:00
3c74b89f15 Restructure diffing code & initial 3-way diffing (WIP) 2024-03-18 18:10:18 -06:00
1343f4fd2b cargo fmt 2024-03-17 12:20:25 -06:00
9df98f263e Move all architecture-specific code into modules
No more scattered relocation handling and
feature checks. Everything will go through
the ObjArch trait, which makes it easier
to add new architectures going forward.
2024-03-17 12:16:47 -06:00
bbe49eb8b4 Initial x86 support
Includes a bit of work to make adding new
architectures easier in the future
2024-03-16 23:30:27 -06:00
aecb078b2a ci: Update sccache-action version 2024-03-13 18:34:28 -06:00
a5668b484b Update all dependencies 2024-03-13 18:20:46 -06:00
ef41e393d4 Resolve dependency advisories 2024-03-04 18:19:08 -07:00
20e42a499a Rework .splitmeta, now .note.split
Uses actual ELF .note format, which is
more standard and handled better by mwld.
2024-03-04 18:06:21 -07:00
c39795ae2c Use actual decomp.me host 2024-03-04 18:03:32 -07:00
49ee9b44aa Remove "Algorithm" menu item 2024-03-04 18:03:20 -07:00
Robin Avery
341c1d4b33 Fix release CI (and add sccache) (#52)
* Fix release CI (and add `sccache`)

* Rename `objdiff-gui` binary to `objdiff`
2024-03-02 22:42:24 -07:00
Robin Avery
9f4a1e86cd objdiff-cli diff: Reduce duplicate key event code (#51) 2024-03-02 18:47:54 -07:00
Robin Avery
ed5d092b11 objdiff-cli diff: Support "Relax relocation diffs" (#50)
Bound to the `-x` flag or the `x` key.
2024-03-02 18:47:18 -07:00
Robin Avery
023dd7a55b objdiff-cli diff: Accept any kind of unit path (#48)
* objdiff-cli diff: Accept any kind of unit path

* Appease clippy

* Call `resolve_paths` in slightly fewer cases
2024-03-01 18:18:27 -07:00
3b1249e1ab objdiff-cli diff: Add horizontal scrolling 2024-03-01 01:30:47 -07:00
cb13638e07 objdiff-cli: Migrate to ratatui for rendering 2024-03-01 01:03:17 -07:00
Robin Avery
37ddbb7f4a cli: Log to stderr instead of stdout (#46)
Fixes pipe issues.
2024-02-29 22:27:10 -07:00
Robin Avery
b80d361e91 cli report: Generate virtual addresses as uppercase (#45)
Matches dtk symbols.txt and most projects' identifiers.
2024-02-29 22:22:59 -07:00
Robin Avery
fd27f4d0cd cli diff: Resolve object and project if not specified (#44)
* cli diff: Resolve object and project if not specified

* Make `symbol` positional

* Short circuit ambiguous matches

* Tighten argument matching

* Speed up function lookup
2024-02-29 22:22:41 -07:00
Robin Avery
5cfd04fd4f Add #[serde(default)] to ReportFunction::address (#43) 2024-02-29 11:21:30 -07:00
5b9ac93c08 ci: Build both objdiff-cli and objdiff-gui 2024-02-28 21:52:35 -07:00
39a13f4d36 objdiff-cli diff & report changes, support .splitmeta object section
- Add `objdiff-cli report changes` for diffing two reports
- Unify some click-to-highlight logic between CLI and GUI
- Load .splitmeta section for extra object metadata (original virtual addr, etc)
- More work on objdiff-cli diff
2024-02-28 21:44:53 -07:00
Ryan Burns
28348606bf Handle ^F, ^B, ^U and ^D readline shortcuts in pager (#42) 2024-02-28 19:33:15 -07:00
fb24063c54 objdiff-cli diff: Click-to-highlight & build fixes 2024-02-27 22:52:18 -07:00
cff6a230a3 Remove alternate diff algorithms, only keep Patience 2024-02-27 21:18:42 -07:00
9a7d2bcebf Experimental objdiff-cli (WIP) 2024-02-27 18:47:51 -07:00
4eba5f71b0 Split into objdiff-core / objdiff-gui; update egui to 0.26.2 2024-02-26 18:48:48 -07:00
0a85c498c5 Version 1.0.0 2024-01-22 00:32:54 -07:00
c2fcf2797b Export function to decomp.me scratch (beta) 2024-01-22 00:31:43 -07:00
e88a58ba39 Option to relax relocation diffs
Ignores differences in relocation targets. (Address, name, etc)

Resolves #34
2024-01-22 00:14:03 -07:00
02f521a528 Disable more options when project config is loaded 2024-01-21 23:58:10 -07:00
197d1247a8 Highlight: Consider uimm/simm/offset all equivalent
Fixes #33
2024-01-21 23:48:12 -07:00
eef9598e76 Add DWARF 2+ line info support
Resolves #37
2024-01-21 23:38:52 -07:00
405a2a82db Upgrade all dependencies (+ egui/eframe 0.25.0) 2024-01-20 23:41:48 -07:00
4cdad8a519 Re-enable wgpu and wsl features; rework WSL config
Improve build failure log view & add copy buttons
2024-01-20 23:29:05 -07:00
b74a49ed0c Upgrade to egui/eframe 0.24.1 2023-12-11 13:36:00 -05:00
e1079db93a Add font loading & configuration 2023-11-28 23:13:51 -05:00
879e03eed5 All one line (thanks PowerShell) 2023-11-27 19:55:17 -05:00
53e6e0c7c4 Build macOS with wgpu enabled 2023-11-27 19:52:12 -05:00
67cea2a8d9 Update README.md 2023-11-24 23:17:35 -05:00
e4f97adbdd Version 0.6.1 2023-11-22 00:11:33 -05:00
0ec7bf078b Highlight: Consider reg offsets and signed immediates equivalent
Fixes #32
2023-11-22 00:07:21 -05:00
74e89130a8 Repaint rework: more responsive, less energy
Previously, we repainted every frame on Windows at full refresh rate.
This is an enormous waste, as the UI will be static most of the time.
This was to work around a bug with `rfd` + `eframe`.
On other platforms, we only repainted every frame when a job was running,
which was better, but still not ideal. We also had a 100ms deadline, so
we'd repaint at ~10fps minimum to catch new events (file watcher, jobs).

This removes all repaint logic from the main loop and moves it into the
individual places where we change state from another thread.
For example, the file watcher thread will now immediately notify egui
to repaint, rather than relying on the 100ms deadline we had previously.
Jobs, when updating their status, also notify egui to repaint.

For `rfd` file dialogs, this migrates to using the async API built on top of
a polling thread + `pollster`. This interacts better with `eframe` on Windows.
Overall, this should reduce repaints and improve responsiveness to
file changes and background tasks.
2023-11-21 14:34:26 -05:00
236e4d8d26 CI updates, update deny.toml, clippy fix 2023-11-21 12:16:15 -05:00
b900ae5a00 Disable WSL integration
With WSL, objdiff is unable to get filesystem notifications.
It's recommended to run objdiff natively on Windows, so having this option
is more confusing than useful.
2023-11-21 12:15:41 -05:00
261e1b8e07 Upgrade all dependencies 2023-11-21 11:57:02 -05:00
a29e913b45 Add "Incomplete" filter to object tree
Allows filtering out objects marked as "complete".
2023-11-21 11:50:11 -05:00
49257dc73c Better logic to reload previous file on app start
Before, if "Rebuild on changes" was disabled, the last file
wouldn't be properly loaded when starting.
2023-11-21 11:49:26 -05:00
dc9eec66b0 Configurable diff algorithms & new default algorithm
Uses the similar crate to support new diff algorithms:
- Patience (new default)
- Levenshtein (old default)
- Myers
- LCS (Longest Common Subsequence)

Options in "Diff Options" -> "Algorithm..."
2023-11-21 11:48:18 -05:00
7b58f9a269 Adjust "Diffable" to exclude missing target objects 2023-10-09 12:47:22 -04:00
d9e7dacb6d Version 0.5.1 2023-10-07 14:49:01 -04:00
04b4fdcd21 Reload objects when changed externally
Uses file modification timestamp polling for project config and objects to avoid unneeded complexity from the filesystem notification watcher.

Allows disabling `build_base` as well for projects using an external build system.
2023-10-07 14:48:34 -04:00
803eaafee6 Hide hidden symbols by default; add "Diff Options" to menu 2023-10-07 13:27:12 -04:00
e1dc84698f Restore context menu on highlightable fields
Fixes #30
2023-10-07 13:04:08 -04:00
e68629c339 Update ppc750cl (subi{,s,c} mnemonics, capstone-style CR bits) 2023-10-06 01:22:26 -04:00
bb9ff4b928 Update all dependencies 2023-10-05 23:55:01 -04:00
57392daaeb Implement click-to-highlight
Highlights registers, instructions, arguments, symbols or addresses on click.

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

* Rename Affix::find parameters to not be string

* Remove unused `LevMatchingBlock` struct

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

* Fix reference problem

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

* Use let-else in diff::reloc_eq

* Use let-else in diff::diff_objs

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

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

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

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

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

@@ -9,8 +9,9 @@ on:
workflow_dispatch:
env:
CARGO_BIN_NAME: objdiff
BUILD_PROFILE: release-lto
CARGO_TARGET_DIR: target
CARGO_INCREMENTAL: 0
jobs:
check:
@@ -20,17 +21,37 @@ jobs:
RUSTFLAGS: -D warnings
steps:
- name: Install dependencies
run: sudo apt-get -y install libgtk-3-dev
run: |
sudo apt-get update
sudo apt-get -y install libgtk-3-dev
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
components: clippy
- name: Cache Rust workspace
uses: Swatinem/rust-cache@v2
- name: Cargo check
run: cargo check --all-features
run: cargo check --all-features --all-targets
- name: Cargo clippy
run: cargo clippy --all-features
run: cargo clippy --all-features --all-targets
fmt:
name: Format
runs-on: ubuntu-latest
env:
RUSTFLAGS: -D warnings
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Rust toolchain
# We use nightly options in rustfmt.toml
uses: dtolnay/rust-toolchain@nightly
with:
components: rustfmt
- name: Cargo fmt
run: cargo fmt --all --check
deny:
name: Deny
@@ -43,13 +64,14 @@ jobs:
# Prevent new advisories from failing CI
continue-on-error: ${{ matrix.checks == 'advisories' }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: EmbarkStudios/cargo-deny-action@v1
with:
command: check ${{ matrix.checks }}
test:
name: Test
if: 'false' # No tests yet
strategy:
matrix:
platform: [ ubuntu-latest, windows-latest, macos-latest ]
@@ -58,16 +80,102 @@ jobs:
steps:
- name: Install dependencies
if: matrix.platform == 'ubuntu-latest'
run: sudo apt-get -y install libgtk-3-dev
run: |
sudo apt-get update
sudo apt-get -y install libgtk-3-dev
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Cache Rust workspace
uses: Swatinem/rust-cache@v2
- name: Cargo test
run: cargo test --release --all-features
build:
name: Build
build-cli:
name: Build objdiff-cli
env:
CARGO_BIN_NAME: objdiff-cli
strategy:
matrix:
include:
- platform: ubuntu-latest
target: x86_64-unknown-linux-musl
name: linux-x86_64
build: zigbuild
features: default
- platform: ubuntu-latest
target: i686-unknown-linux-musl
name: linux-i686
build: zigbuild
features: default
- platform: ubuntu-latest
target: aarch64-unknown-linux-musl
name: linux-aarch64
build: zigbuild
features: default
- platform: windows-latest
target: i686-pc-windows-msvc
name: windows-x86
build: build
features: default
- platform: windows-latest
target: x86_64-pc-windows-msvc
name: windows-x86_64
build: build
features: default
- platform: windows-latest
target: aarch64-pc-windows-msvc
name: windows-arm64
build: build
features: default
- platform: macos-latest
target: x86_64-apple-darwin
name: macos-x86_64
build: build
features: default
- platform: macos-latest
target: aarch64-apple-darwin
name: macos-arm64
build: build
features: default
fail-fast: false
runs-on: ${{ matrix.platform }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install cargo-zigbuild
if: matrix.build == 'zigbuild'
run: |
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
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Cache Rust workspace
uses: Swatinem/rust-cache@v2
with:
key: ${{ matrix.target }}
- name: Cargo build
run: >
cargo ${{ matrix.build }} --profile ${{ env.BUILD_PROFILE }} --target ${{ matrix.target }}
--bin ${{ env.CARGO_BIN_NAME }} --features ${{ matrix.features }}
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: ${{ env.CARGO_BIN_NAME }}-${{ matrix.name }}
path: |
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}.exe
if-no-files-found: error
build-gui:
name: Build objdiff-gui
env:
CARGO_BIN_NAME: objdiff
strategy:
matrix:
include:
@@ -75,59 +183,101 @@ jobs:
target: x86_64-unknown-linux-gnu
name: linux-x86_64
packages: libgtk-3-dev
features: default
- platform: windows-latest
target: x86_64-pc-windows-msvc
name: windows-x86_64
features: default
- platform: macos-latest
target: x86_64-apple-darwin
name: macos-x86_64
features: default
- platform: macos-latest
target: aarch64-apple-darwin
name: macos-arm64
features: default
fail-fast: false
runs-on: ${{ matrix.platform }}
steps:
- name: Install dependencies
if: matrix.packages != ''
run: sudo apt-get -y install ${{ matrix.packages }}
run: |
sudo apt-get update
sudo apt-get -y install ${{ matrix.packages }}
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Cargo build
run: cargo build --release --all-features --target ${{ matrix.target }} --bin ${{ env.CARGO_BIN_NAME }}
- name: Upload artifacts
uses: actions/upload-artifact@v3
- name: Cache Rust workspace
uses: Swatinem/rust-cache@v2
with:
name: ${{ matrix.name }}
key: ${{ matrix.target }}
- name: Cargo build
run: >
cargo build --profile ${{ env.BUILD_PROFILE }} --target ${{ matrix.target }}
--bin ${{ env.CARGO_BIN_NAME }} --features ${{ matrix.features }}
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: ${{ env.CARGO_BIN_NAME }}-${{ matrix.name }}
path: |
${{ env.CARGO_TARGET_DIR }}/release/${{ env.CARGO_BIN_NAME }}
${{ env.CARGO_TARGET_DIR }}/release/${{ env.CARGO_BIN_NAME }}.exe
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/release/${{ env.CARGO_BIN_NAME }}
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/release/${{ env.CARGO_BIN_NAME }}.exe
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}.exe
if-no-files-found: error
release:
name: Release
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
needs: [ build ]
needs: [ build-cli, build-gui ]
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Check git tag against Cargo version
shell: bash
run: |
set -eou pipefail
tag='${{github.ref}}'
tag="${tag#refs/tags/}"
version=$(grep '^version' Cargo.toml | head -1 | awk -F' = ' '{print $2}' | tr -d '"')
version="v$version"
if [ "$tag" != "$version" ]; then
echo "::error::Git tag doesn't match the Cargo version! ($tag != $version)"
exit 1
fi
- name: Download artifacts
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Rename artifacts
working-directory: artifacts
run: |
set -euo pipefail
mkdir ../out
for i in */*/release/$CARGO_BIN_NAME*; do
mv "$i" "../out/$(sed -E "s/([^/]+)\/[^/]+\/release\/($CARGO_BIN_NAME)/\2-\1/" <<< "$i")"
for dir in */; do
for file in "$dir"*; do
base=$(basename "$file")
name="${base%.*}"
ext="${base##*.}"
if [ "$ext" = "$base" ]; then
ext=""
else
ext=".$ext"
fi
arch="${dir%/}" # remove trailing slash
arch="${arch##"$name-"}" # remove bin name
dst="../out/${name}-${arch}${ext}"
mv "$file" "$dst"
done
done
ls -R ../out
- name: Release
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@v2
with:
files: out/*
draft: true
generate_release_notes: true

5
.gitignore vendored
View File

@@ -3,10 +3,6 @@ target/
**/*.rs.bk
generated/
# cargo-mobile
.cargo/
/gen
# macOS
.DS_Store
@@ -22,3 +18,4 @@ android.keystore
*.frag
*.vert
*.metal
.vscode/

5318
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,62 +1,21 @@
[package]
name = "objdiff"
version = "0.2.0"
edition = "2021"
rust-version = "1.62"
[workspace]
members = [
"objdiff-cli",
"objdiff-core",
"objdiff-gui",
]
resolver = "2"
[profile.release-lto]
inherits = "release"
lto = "fat"
strip = "debuginfo"
codegen-units = 1
[workspace.package]
version = "2.4.0"
authors = ["Luke Street <luke@street.dev>"]
edition = "2021"
license = "MIT OR Apache-2.0"
repository = "https://github.com/encounter/objdiff"
readme = "README.md"
description = """
A local diffing tool for decompilation projects.
"""
publish = false
[profile.release]
lto = "thin"
strip = "debuginfo"
[dependencies]
anyhow = "1.0.66"
cfg-if = "1.0.0"
const_format = "0.2.30"
cwdemangle = { git = "https://github.com/encounter/cwdemangle", rev = "286f3d1d29ee2457db89043782725631845c3e4c" }
eframe = { version = "0.19.0", features = ["persistence"] } # , "wgpu"
egui = "0.19.0"
egui_extras = "0.19.0"
flagset = "0.4.3"
log = "0.4.17"
memmap2 = "0.5.8"
notify = "5.0.0"
object = { version = "0.30.0", features = ["read_core", "std", "elf"], default-features = false }
ppc750cl = { git = "https://github.com/encounter/ppc750cl", rev = "aa631a33de7882c679afca89350898b87cb3ba3f" }
rabbitizer = { git = "https://github.com/encounter/rabbitizer-rs", rev = "10c279b2ef251c62885b1dcdcfe740b0db8e9956" }
rfd = { version = "0.10.0" } # , default-features = false, features = ['xdg-portal']
self_update = "0.32.0"
serde = { version = "1", features = ["derive"] }
thiserror = "1.0.37"
time = { version = "0.3.17", features = ["formatting", "local-offset"] }
toml = "0.5.9"
twox-hash = "1.6.3"
tempfile = "3.3.0"
reqwest = "0.11.13"
[target.'cfg(windows)'.dependencies]
path-slash = "0.2.1"
winapi = "0.3.9"
[target.'cfg(unix)'.dependencies]
exec = "0.3.1"
# native:
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
tracing-subscriber = "0.3"
# web:
[target.'cfg(target_arch = "wasm32")'.dependencies]
console_error_panic_hook = "0.1.7"
tracing-wasm = "0.2"
[build-dependencies]
anyhow = "1.0.66"
vergen = { version = "7.4.3", features = ["build", "cargo", "git"], default-features = false }
rust-version = "1.74"

180
README.md
View File

@@ -3,26 +3,186 @@
[Build Status]: https://github.com/encounter/objdiff/actions/workflows/build.yaml/badge.svg
[actions]: https://github.com/encounter/objdiff/actions
A local diffing tool for decompilation projects.
A local diffing tool for decompilation projects. Inspired by [decomp.me](https://decomp.me) and [asm-differ](https://github.com/simonlindholm/asm-differ).
Currently supports:
- PowerPC 750CL (GameCube & Wii)
- MIPS (Nintendo 64)
Features:
- Compare entire object files: functions and data.
- Built-in symbol demangling for C++. (CodeWarrior, Itanium & MSVC)
- Automatic rebuild on source file changes.
- Project integration via [configuration file](#configuration).
- Search and filter all of a project's objects and quickly switch.
- Click to highlight all instances of values and registers.
Supports:
- PowerPC 750CL (GameCube, Wii)
- MIPS (N64, PS1, PS2, PSP)
- x86 (COFF only at the moment)
- ARM (GBA, DS, 3DS)
- ARM64 (Switch, experimental)
See [Usage](#usage) for more information.
## Downloads
To build from source, see [Building](#building).
### GUI
- [Windows (x86_64)](https://github.com/encounter/objdiff/releases/latest/download/objdiff-windows-x86_64.exe)
- [Linux (x86_64)](https://github.com/encounter/objdiff/releases/latest/download/objdiff-linux-x86_64)
- [macOS (arm64)](https://github.com/encounter/objdiff/releases/latest/download/objdiff-macos-arm64)
- [macOS (x86_64)](https://github.com/encounter/objdiff/releases/latest/download/objdiff-macos-x86_64)
For Linux and macOS, run `chmod +x objdiff-*` to make the binary executable.
### CLI
CLI binaries can be found on the [releases page](https://github.com/encounter/objdiff/releases).
## Screenshots
![Symbol Screenshot](assets/screen-symbols.png)
![Diff Screenshot](assets/screen-diff.png)
### License
## Usage
objdiff works by comparing two relocatable object files (`.o`). The objects are expected to have the same relative path
from the "target" and "base" directories.
For example, if the target ("expected") object is located at `build/asm/MetroTRK/mslsupp.o` and the base ("actual")
object is located at `build/src/MetroTRK/mslsupp.o`, the following configuration would be used:
- Target build directory: `build/asm`
- Base build directory: `build/src`
- Object: `MetroTRK/mslsupp.o`
objdiff will then execute the build system from the project directory to build both objects:
```sh
$ make build/asm/MetroTRK/mslsupp.o # Only if "Build target object" is enabled
$ make build/src/MetroTRK/mslsupp.o
```
The objects will then be compared and the results will be displayed in the UI.
See [Configuration](#configuration) for more information.
## Configuration
While **not required** (most settings can be specified in the UI), projects can add an `objdiff.json` file to configure the tool automatically. The configuration file must be located in
the root project directory.
If your project has a generator script (e.g. `configure.py`), it's recommended to generate the objdiff configuration
file as well. You can then add `objdiff.json` to your `.gitignore` to prevent it from being committed.
```json
{
"$schema": "https://raw.githubusercontent.com/encounter/objdiff/main/config.schema.json",
"custom_make": "ninja",
"custom_args": [
"-d",
"keeprsp"
],
"build_target": false,
"build_base": true,
"watch_patterns": [
"*.c",
"*.cp",
"*.cpp",
"*.cxx",
"*.h",
"*.hp",
"*.hpp",
"*.hxx",
"*.s",
"*.S",
"*.asm",
"*.inc",
"*.py",
"*.yml",
"*.txt",
"*.json"
],
"units": [
{
"name": "main/MetroTRK/mslsupp",
"target_path": "build/asm/MetroTRK/mslsupp.o",
"base_path": "build/src/MetroTRK/mslsupp.o",
"metadata": {}
}
]
}
```
### Schema
View [config.schema.json](config.schema.json) for all available options. The below list is a summary of the most important options.
`custom_make` _(optional)_: By default, objdiff will use `make` to build the project.
If the project uses a different build system (e.g. `ninja`), specify it here.
The build command will be `[custom_make] [custom_args] path/to/object.o`.
`custom_args` _(optional)_: Additional arguments to pass to the build command prior to the object path.
`build_target`: If true, objdiff will tell the build system to build the target objects before diffing (e.g.
`make path/to/target.o`).
This is useful if the target objects are not built by default or can change based on project configuration or edits
to assembly files.
Requires the build system to be configured properly.
`build_base`: If true, objdiff will tell the build system to build the base objects before diffing (e.g. `make path/to/base.o`).
It's unlikely you'll want to disable this, unless you're using an external tool to rebuild the base object on source file changes.
`watch_patterns` _(optional)_: A list of glob patterns to watch for changes.
([Supported syntax](https://docs.rs/globset/latest/globset/#syntax))
If any of these files change, objdiff will automatically rebuild the objects and re-compare them.
If not specified, objdiff will use the default patterns listed above.
`units` _(optional)_: If specified, objdiff will display a list of objects in the sidebar for easy navigation.
> `name` _(optional)_: The name of the object in the UI. If not specified, the object's `path` will be used.
>
> `target_path`: Path to the "target" or "expected" object from the project root.
> This object is the **intended result** of the match.
>
> `base_path`: Path to the "base" or "actual" object from the project root.
> This object is built from the **current source code**.
>
> `metadata.auto_generated` _(optional)_: Hides the object from the object list, but still includes it in reports.
>
> `metadata.complete` _(optional)_: Marks the object as "complete" (or "linked") in the object list.
> This is useful for marking objects that are fully decompiled. A value of `false` will mark the object as "incomplete".
## Building
Install Rust via [rustup](https://rustup.rs).
```shell
$ git clone https://github.com/encounter/objdiff.git
$ cd objdiff
$ cargo run --release
```
Or using `cargo install`.
```shell
$ cargo install --locked --git https://github.com/encounter/objdiff.git objdiff-gui objdiff-cli
```
The binaries will be installed to `~/.cargo/bin` as `objdiff` and `objdiff-cli`.
## License
Licensed under either of
* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
* MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
- Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or <http://www.apache.org/licenses/LICENSE-2.0>)
- MIT license ([LICENSE-MIT](LICENSE-MIT) or <http://opensource.org/licenses/MIT>)
at your option.
### Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any
additional terms or conditions.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as
defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

After

Width:  |  Height:  |  Size: 144 KiB

View File

@@ -1,4 +0,0 @@
use anyhow::Result;
use vergen::{vergen, Config};
fn main() -> Result<()> { vergen(Config::default()) }

228
config.schema.json Normal file
View File

@@ -0,0 +1,228 @@
{
"$schema": "https://json-schema.org/draft-07/schema",
"$id": "https://raw.githubusercontent.com/encounter/objdiff/main/config.schema.json",
"title": "objdiff configuration",
"description": "Configuration file for objdiff",
"type": "object",
"properties": {
"min_version": {
"type": "string",
"description": "Minimum version of objdiff required to load this configuration file.",
"examples": [
"1.0.0",
"2.0.0-beta.1"
]
},
"custom_make": {
"type": "string",
"description": "By default, objdiff will use make to build the project.\nIf the project uses a different build system (e.g. ninja), specify it here.\nThe build command will be `[custom_make] [custom_args] path/to/object.o`.",
"examples": [
"make",
"ninja"
],
"default": "make"
},
"custom_args": {
"type": "array",
"description": "Additional arguments to pass to the build command prior to the object path.",
"items": {
"type": "string"
}
},
"target_dir": {
"type": "string",
"description": "Relative from the root of the project, this where the \"target\" or \"expected\" objects are located.\nThese are the intended result of the match.",
"deprecated": true
},
"base_dir": {
"type": "string",
"description": "Relative from the root of the project, this is where the \"base\" or \"actual\" objects are located.\nThese are objects built from the current source code.",
"deprecated": true
},
"build_target": {
"type": "boolean",
"description": "If true, objdiff will tell the build system to build the target objects before diffing (e.g. `make path/to/target.o`).\nThis is useful if the target objects are not built by default or can change based on project configuration or edits to assembly files.\nRequires the build system to be configured properly.",
"default": false
},
"build_base": {
"type": "boolean",
"description": "If true, objdiff will tell the build system to build the base objects before diffing (e.g. `make path/to/base.o`).\nIt's unlikely you'll want to disable this, unless you're using an external tool to rebuild the base object on source file changes.",
"default": true
},
"watch_patterns": {
"type": "array",
"description": "List of glob patterns to watch for changes in the project.\nIf any of these files change, objdiff will automatically rebuild the objects and re-compare them.\nSupported syntax: https://docs.rs/globset/latest/globset/#syntax",
"items": {
"type": "string"
},
"default": [
"*.c",
"*.cp",
"*.cpp",
"*.cxx",
"*.h",
"*.hp",
"*.hpp",
"*.hxx",
"*.s",
"*.S",
"*.asm",
"*.inc",
"*.py",
"*.yml",
"*.txt",
"*.json"
]
},
"objects": {
"type": "array",
"description": "Use units instead.",
"deprecated": true,
"items": {
"$ref": "#/$defs/unit"
}
},
"units": {
"type": "array",
"description": "If specified, objdiff will display a list of objects in the sidebar for easy navigation.",
"items": {
"$ref": "#/$defs/unit"
}
},
"progress_categories": {
"type": "array",
"description": "Progress categories used for objdiff-cli report.",
"items": {
"$ref": "#/$defs/progress_category"
}
}
},
"$defs": {
"unit": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The name of the object in the UI. If not specified, the object's path will be used."
},
"path": {
"type": "string",
"description": "Relative path to the object from the target_dir and base_dir.\nRequires target_dir and base_dir to be specified.",
"deprecated": true
},
"target_path": {
"type": "string",
"description": "Path to the target object from the project root.\nRequired if path is not specified."
},
"base_path": {
"type": "string",
"description": "Path to the base object from the project root.\nRequired if path is not specified."
},
"reverse_fn_order": {
"type": "boolean",
"description": "Displays function symbols in reversed order.\nUsed to support MWCC's -inline deferred option, which reverses the order of functions in the object file.",
"deprecated": true
},
"complete": {
"type": "boolean",
"description": "Marks the object as \"complete\" (or \"linked\") in the object list.\nThis is useful for marking objects that are fully decompiled. A value of `false` will mark the object as \"incomplete\".",
"deprecated": true
},
"scratch": {
"ref": "#/$defs/scratch"
},
"metadata": {
"ref": "#/$defs/metadata"
},
"symbol_mappings": {
"type": "object",
"description": "Manual symbol mappings from target to base.",
"additionalProperties": {
"type": "string"
}
}
}
},
"scratch": {
"type": "object",
"description": "If present, objdiff will display a button to create a decomp.me scratch.",
"properties": {
"platform": {
"type": "string",
"description": "The decomp.me platform ID to use for the scratch.",
"examples": [
"gc_wii",
"n64"
]
},
"compiler": {
"type": "string",
"description": "The decomp.me compiler ID to use for the scratch.",
"examples": [
"mwcc_242_81",
"ido7.1"
]
},
"c_flags": {
"type": "string",
"description": "C flags to use for the scratch. Exclude any include paths."
},
"ctx_path": {
"type": "string",
"description": "Path to the context file to use for the scratch."
},
"build_ctx": {
"type": "boolean",
"description": "If true, objdiff will run the build command with the context file as an argument to generate it.",
"default": false
}
},
"required": [
"platform",
"compiler"
]
},
"metadata": {
"type": "object",
"properties": {
"complete": {
"type": "boolean",
"description": "Marks the object as \"complete\" (or \"linked\") in the object list.\nThis is useful for marking objects that are fully decompiled. A value of `false` will mark the object as \"incomplete\"."
},
"reverse_fn_order": {
"type": "boolean",
"description": "Displays function symbols in reversed order.\nUsed to support MWCC's -inline deferred option, which reverses the order of functions in the object file."
},
"source_path": {
"type": "string",
"description": "Path to the source file that generated the object."
},
"progress_categories": {
"type": "array",
"description": "Progress categories used for objdiff-cli report.",
"items": {
"type": "string",
"description": "Unique identifier for the category. (See progress_categories)"
}
},
"auto_generated": {
"type": "boolean",
"description": "Hides the object from the object list by default, but still includes it in reports."
}
}
},
"progress_category": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Unique identifier for the category."
},
"name": {
"type": "string",
"description": "Human-readable name of the category."
}
}
}
}
}

186
deny.toml
View File

@@ -9,6 +9,11 @@
# The values provided in this template are the default values that will be used
# when any section or field is not specified in your own configuration
# Root options
# The graph table configures how the dependency graph is constructed and thus
# which crates the checks are performed against
[graph]
# If 1 or more target triples (and optionally, target_features) are specified,
# only the specified targets will be checked when running `cargo deny check`.
# This means, if a particular package is only ever used as a target specific
@@ -20,88 +25,86 @@
targets = [
# The triple can be any string, but only the target triples built in to
# rustc (as of 1.40) can be checked against actual config expressions
#{ triple = "x86_64-unknown-linux-musl" },
#"x86_64-unknown-linux-musl",
# You can also specify which target_features you promise are enabled for a
# particular target. target_features are currently not validated against
# the actual valid features supported by the target architecture.
#{ triple = "wasm32-unknown-unknown", features = ["atomics"] },
]
# When creating the dependency graph used as the source of truth when checks are
# executed, this field can be used to prune crates from the graph, removing them
# from the view of cargo-deny. This is an extremely heavy hammer, as if a crate
# is pruned from the graph, all of its dependencies will also be pruned unless
# they are connected to another crate in the graph that hasn't been pruned,
# so it should be used with care. The identifiers are [Package ID Specifications]
# (https://doc.rust-lang.org/cargo/reference/pkgid-spec.html)
#exclude = []
# If true, metadata will be collected with `--all-features`. Note that this can't
# be toggled off if true, if you want to conditionally enable `--all-features` it
# is recommended to pass `--all-features` on the cmd line instead
all-features = false
# If true, metadata will be collected with `--no-default-features`. The same
# caveat with `all-features` applies
no-default-features = false
# If set, these feature will be enabled when collecting metadata. If `--features`
# is specified on the cmd line they will take precedence over this option.
#features = []
# The output table provides options for how/if diagnostics are outputted
[output]
# When outputting inclusion graphs in diagnostics that include features, this
# option can be used to specify the depth at which feature edges will be added.
# This option is included since the graphs can be quite large and the addition
# of features from the crate(s) to all of the graph roots can be far too verbose.
# This option can be overridden via `--feature-depth` on the cmd line
feature-depth = 1
# This section is considered when running `cargo deny check advisories`
# More documentation for the advisories section can be found here:
# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html
[advisories]
# The path where the advisory database is cloned/fetched into
db-path = "~/.cargo/advisory-db"
# The path where the advisory databases are cloned/fetched into
#db-path = "$CARGO_HOME/advisory-dbs"
# The url(s) of the advisory databases to use
db-urls = ["https://github.com/rustsec/advisory-db"]
# The lint level for security vulnerabilities
vulnerability = "deny"
# The lint level for unmaintained crates
unmaintained = "warn"
# The lint level for crates that have been yanked from their source registry
yanked = "warn"
# The lint level for crates with security notices. Note that as of
# 2019-12-17 there are no security notice advisories in
# https://github.com/rustsec/advisory-db
notice = "warn"
#db-urls = ["https://github.com/rustsec/advisory-db"]
# A list of advisory IDs to ignore. Note that ignored advisories will still
# output a note when they are encountered.
ignore = [
#"RUSTSEC-0000-0000",
#{ id = "RUSTSEC-0000-0000", reason = "you can specify a reason the advisory is ignored" },
#"a-crate-that-is-yanked@0.1.1", # you can also ignore yanked crate versions if you wish
#{ crate = "a-crate-that-is-yanked@0.1.1", reason = "you can specify why you are ignoring the yanked crate" },
{ id = "RUSTSEC-2024-0384", reason = "Unmaintained indirect dependency" },
]
# Threshold for security vulnerabilities, any vulnerability with a CVSS score
# lower than the range specified will be ignored. Note that ignored advisories
# will still output a note when they are encountered.
# * None - CVSS Score 0.0
# * Low - CVSS Score 0.1 - 3.9
# * Medium - CVSS Score 4.0 - 6.9
# * High - CVSS Score 7.0 - 8.9
# * Critical - CVSS Score 9.0 - 10.0
#severity-threshold =
# If this is true, then cargo deny will use the git executable to fetch advisory database.
# If this is false, then it uses a built-in git library.
# Setting this to true can be helpful if you have special authentication requirements that cargo-deny does not support.
# See Git Authentication for more information about setting up git authentication.
#git-fetch-with-cli = true
# This section is considered when running `cargo deny check licenses`
# More documentation for the licenses section can be found here:
# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html
[licenses]
# The lint level for crates which do not have a detectable license
unlicensed = "deny"
# List of explictly allowed licenses
# List of explicitly allowed licenses
# See https://spdx.org/licenses/ for list of possible licenses
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
allow = [
"MIT",
"Apache-2.0",
"Apache-2.0 WITH LLVM-exception",
"ISC",
"BSD-2-Clause",
"BSD-3-Clause",
"BSL-1.0",
"CC0-1.0",
"MPL-2.0",
"Unicode-DFS-2016",
"Unicode-3.0",
"Zlib",
"0BSD",
"OFL-1.1",
"LicenseRef-UFL-1.0",
"OpenSSL",
]
# List of explictly disallowed licenses
# See https://spdx.org/licenses/ for list of possible licenses
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
deny = [
#"Nokia",
]
# Lint level for licenses considered copyleft
copyleft = "warn"
# Blanket approval or denial for OSI-approved or FSF Free/Libre licenses
# * both - The license will be approved if it is both OSI-approved *AND* FSF
# * either - The license will be approved if it is either OSI-approved *OR* FSF
# * osi-only - The license will be approved if is OSI-approved *AND NOT* FSF
# * fsf-only - The license will be approved if is FSF *AND NOT* OSI-approved
# * neither - This predicate is ignored and the default lint level is used
allow-osi-fsf-free = "neither"
# Lint level used when no other predicates are matched
# 1. License isn't in the allow or deny lists
# 2. License isn't copyleft
# 3. License isn't OSI/FSF, or allow-osi-fsf-free = "neither"
default = "deny"
# The confidence threshold for detecting a license from license text.
# The higher the value, the more closely the license text must be to the
# canonical license text of a valid SPDX license file.
@@ -112,32 +115,32 @@ confidence-threshold = 0.8
exceptions = [
# Each entry is the crate and version constraint, and its specific allow
# list
#{ allow = ["Zlib"], name = "adler32", version = "*" },
#{ allow = ["Zlib"], crate = "adler32" },
]
# Some crates don't have (easily) machine readable licensing information,
# adding a clarification entry for it allows you to manually specify the
# licensing information
#[[licenses.clarify]]
# The name of the crate the clarification applies to
#name = "ring"
# The optional version constraint for the crate
#version = "*"
[[licenses.clarify]]
# The package spec the clarification applies to
crate = "ring"
# The SPDX expression for the license requirements of the crate
#expression = "MIT AND ISC AND OpenSSL"
expression = "MIT AND ISC AND OpenSSL"
# One or more files in the crate's source used as the "source of truth" for
# the license expression. If the contents match, the clarification will be used
# when running the license check, otherwise the clarification will be ignored
# and the crate will be checked normally, which may produce warnings or errors
# depending on the rest of your configuration
#license-files = [
license-files = [
# Each entry is a crate relative path, and the (opaque) hash of its contents
#{ path = "LICENSE", hash = 0xbd0eed23 }
#]
{ path = "LICENSE", hash = 0xbd0eed23 }
]
[licenses.private]
# If true, ignores workspace crates that aren't published, or are only
# published to private registries
# published to private registries.
# To see how to mark a crate as unpublished (to the official registry),
# visit https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field.
ignore = false
# One or more private registries that you might publish crates to, if a crate
# is only published to private registries, and ignore is true, the crate will
@@ -151,7 +154,7 @@ registries = [
# https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html
[bans]
# Lint level for when multiple versions of the same crate are detected
multiple-versions = "warn"
multiple-versions = "allow"
# Lint level for when a crate version requirement is `*`
wildcards = "allow"
# The graph highlighting used when creating dotgraphs for crates
@@ -160,30 +163,63 @@ wildcards = "allow"
# * simplest-path - The path to the version with the fewest edges is highlighted
# * all - Both lowest-version and simplest-path are used
highlight = "all"
# The default lint level for `default` features for crates that are members of
# the workspace that is being checked. This can be overridden by allowing/denying
# `default` on a crate-by-crate basis if desired.
workspace-default-features = "allow"
# The default lint level for `default` features for external crates that are not
# members of the workspace. This can be overridden by allowing/denying `default`
# on a crate-by-crate basis if desired.
external-default-features = "allow"
# List of crates that are allowed. Use with care!
allow = [
#{ name = "ansi_term", version = "=0.11.0" },
#"ansi_term@0.11.0",
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is allowed" },
]
# List of crates to deny
deny = [
# Each entry the name of a crate and a version range. If version is
# not specified, all versions will be matched.
#{ name = "ansi_term", version = "=0.11.0" },
#
#"ansi_term@0.11.0",
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is banned" },
# Wrapper crates can optionally be specified to allow the crate when it
# is a direct dependency of the otherwise banned crate
#{ name = "ansi_term", version = "=0.11.0", wrappers = [] },
#{ crate = "ansi_term@0.11.0", wrappers = ["this-crate-directly-depends-on-ansi_term"] },
]
# List of features to allow/deny
# Each entry the name of a crate and a version range. If version is
# not specified, all versions will be matched.
#[[bans.features]]
#crate = "reqwest"
# Features to not allow
#deny = ["json"]
# Features to allow
#allow = [
# "rustls",
# "__rustls",
# "__tls",
# "hyper-rustls",
# "rustls",
# "rustls-pemfile",
# "rustls-tls-webpki-roots",
# "tokio-rustls",
# "webpki-roots",
#]
# If true, the allowed features must exactly match the enabled feature set. If
# this is set there is no point setting `deny`
#exact = true
# Certain crates/versions that will be skipped when doing duplicate detection.
skip = [
#{ name = "ansi_term", version = "=0.11.0" },
#"ansi_term@0.11.0",
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason why it can't be updated/removed" },
]
# Similarly to `skip` allows you to skip certain crates during duplicate
# detection. Unlike skip, it also includes the entire tree of transitive
# dependencies starting at the specified crate, up to a certain depth, which is
# by default infinite
# by default infinite.
skip-tree = [
#{ name = "ansi_term", version = "=0.11.0", depth = 20 },
#"ansi_term@0.11.0", # will be skipped along with _all_ of its direct and transitive dependencies
#{ crate = "ansi_term@0.11.0", depth = 20 },
]
# This section is considered when running `cargo deny check sources`.
@@ -203,9 +239,9 @@ allow-registry = ["https://github.com/rust-lang/crates.io-index"]
allow-git = []
[sources.allow-org]
# 1 or more github.com organizations to allow git sources for
github = ["encounter"]
# 1 or more gitlab.com organizations to allow git sources for
#gitlab = [""]
# 1 or more bitbucket.org organizations to allow git sources for
#bitbucket = [""]
# github.com organizations to allow git sources for
github = ["notify-rs"]
# gitlab.com organizations to allow git sources for
gitlab = []
# bitbucket.org organizations to allow git sources for
bitbucket = []

33
objdiff-cli/Cargo.toml Normal file
View File

@@ -0,0 +1,33 @@
[package]
name = "objdiff-cli"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
readme = "../README.md"
description = """
A local diffing tool for decompilation projects.
"""
publish = false
[dependencies]
anyhow = "1.0"
argp = "0.3"
crossterm = "0.28"
enable-ansi-support = "0.2"
memmap2 = "0.9"
objdiff-core = { path = "../objdiff-core", features = ["all"] }
prost = "0.13"
ratatui = "0.29"
rayon = "1.10"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
supports-color = "3.0"
time = { version = "0.3", features = ["formatting", "local-offset"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
[target.'cfg(target_env = "musl")'.dependencies]
mimalloc = "0.1"

View File

@@ -0,0 +1,63 @@
// Originally from https://gist.github.com/suluke/e0c672492126be0a4f3b4f0e1115d77c
//! Extend `argp` to be better integrated with the `cargo` ecosystem
//!
//! For now, this only adds a --version/-V option which causes early-exit.
use std::ffi::OsStr;
use argp::{parser::ParseGlobalOptions, EarlyExit, FromArgs, TopLevelCommand};
struct ArgsOrVersion<T>(T)
where T: FromArgs;
impl<T> TopLevelCommand for ArgsOrVersion<T> where T: FromArgs {}
impl<T> FromArgs for ArgsOrVersion<T>
where T: FromArgs
{
fn _from_args(
command_name: &[&str],
args: &[&OsStr],
parent: Option<&mut dyn ParseGlobalOptions>,
) -> Result<Self, EarlyExit> {
/// Also use argp for catching `--version`-only invocations
#[derive(FromArgs)]
struct Version {
/// Print version information and exit.
#[argp(switch, short = 'V')]
pub version: bool,
}
match Version::from_args(command_name, args) {
Ok(v) => {
if v.version {
println!(
"{} {}",
command_name.first().unwrap_or(&""),
env!("CARGO_PKG_VERSION"),
);
std::process::exit(0);
} else {
// Pass through empty arguments
T::_from_args(command_name, args, parent).map(Self)
}
}
Err(exit) => match exit {
EarlyExit::Help(_help) => {
// TODO: Chain help info from Version
// For now, we just put the switch on T as well
T::from_args(command_name, &["--help"]).map(Self)
}
EarlyExit::Err(_) => T::_from_args(command_name, args, parent).map(Self),
},
}
}
}
/// Create a `FromArgs` type from the current processs `env::args`.
///
/// This function will exit early from the current process if argument parsing was unsuccessful or if information like `--help` was requested.
/// Error messages will be printed to stderr, and `--help` output to stdout.
pub fn from_env<T>() -> T
where T: TopLevelCommand {
argp::parse_args_or_exit::<ArgsOrVersion<T>>(argp::DEFAULT).0
}

969
objdiff-cli/src/cmd/diff.rs Normal file
View File

@@ -0,0 +1,969 @@
use std::{
fs,
io::stdout,
path::{Path, PathBuf},
str::FromStr,
};
use anyhow::{bail, Context, Result};
use argp::FromArgs;
use crossterm::{
event,
event::{
DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind, MouseButton,
MouseEventKind,
},
terminal::{
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, SetTitle,
},
};
use event::KeyModifiers;
use objdiff_core::{
bindings::diff::DiffResult,
config::{ProjectConfig, ProjectObject},
diff,
diff::{
display::{display_diff, DiffText, HighlightKind},
DiffObjsResult, ObjDiff, ObjInsDiffKind, ObjSymbolDiff,
},
obj,
obj::{ObjInfo, ObjSectionKind, ObjSymbol, SymbolRef},
};
use ratatui::{
prelude::*,
widgets::{Block, Borders, Clear, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState},
};
use crate::util::{
output::{write_output, OutputFormat},
term::crossterm_panic_handler,
};
#[derive(FromArgs, PartialEq, Debug)]
/// Diff two object files. (Interactive or one-shot mode)
#[argp(subcommand, name = "diff")]
pub struct Args {
#[argp(option, short = '1')]
/// Target object file
target: Option<PathBuf>,
#[argp(option, short = '2')]
/// Base object file
base: Option<PathBuf>,
#[argp(option, short = 'p')]
/// Project directory
project: Option<PathBuf>,
#[argp(option, short = 'u')]
/// Unit name within project
unit: Option<String>,
#[argp(switch, short = 'x')]
/// Relax relocation diffs
relax_reloc_diffs: bool,
#[argp(option, short = 'o')]
/// Output file (one-shot mode) ("-" for stdout)
output: Option<PathBuf>,
#[argp(option)]
/// Output format (json, json-pretty, proto) (default: json)
format: Option<String>,
#[argp(positional)]
/// Function symbol to diff
symbol: Option<String>,
}
pub fn run(args: Args) -> Result<()> {
let (target_path, base_path, project_config) = match (
&args.target,
&args.base,
&args.project,
&args.unit,
) {
(Some(t), Some(b), None, None) => (Some(t.clone()), Some(b.clone()), None),
(None, None, p, u) => {
let project = match p {
Some(project) => project.clone(),
_ => std::env::current_dir().context("Failed to get the current directory")?,
};
let Some((project_config, project_config_info)) =
objdiff_core::config::try_project_config(&project)
else {
bail!("Project config not found in {}", &project.display())
};
let mut project_config = project_config.with_context(|| {
format!("Reading project config {}", project_config_info.path.display())
})?;
let object = {
let resolve_paths = |o: &mut ProjectObject| {
o.resolve_paths(
&project,
project_config.target_dir.as_deref(),
project_config.base_dir.as_deref(),
)
};
if let Some(u) = u {
let unit_path =
PathBuf::from_str(u).ok().and_then(|p| fs::canonicalize(p).ok());
let Some(object) = project_config
.units
.as_deref_mut()
.unwrap_or_default()
.iter_mut()
.find_map(|obj| {
if obj.name.as_deref() == Some(u) {
resolve_paths(obj);
return Some(obj);
}
let up = unit_path.as_deref()?;
resolve_paths(obj);
if [&obj.base_path, &obj.target_path]
.into_iter()
.filter_map(|p| p.as_ref().and_then(|p| p.canonicalize().ok()))
.any(|p| p == up)
{
return Some(obj);
}
None
})
else {
bail!("Unit not found: {}", u)
};
object
} else if let Some(symbol_name) = &args.symbol {
let mut idx = None;
let mut count = 0usize;
for (i, obj) in project_config
.units
.as_deref_mut()
.unwrap_or_default()
.iter_mut()
.enumerate()
{
resolve_paths(obj);
if obj
.target_path
.as_deref()
.map(|o| obj::read::has_function(o, symbol_name))
.transpose()?
.unwrap_or(false)
{
idx = Some(i);
count += 1;
if count > 1 {
break;
}
}
}
match (count, idx) {
(0, None) => bail!("Symbol not found: {}", symbol_name),
(1, Some(i)) => &mut project_config.units_mut()[i],
(2.., Some(_)) => bail!(
"Multiple instances of {} were found, try specifying a unit",
symbol_name
),
_ => unreachable!(),
}
} else {
bail!("Must specify one of: symbol, project and unit, target and base objects")
}
};
let target_path = object.target_path.clone();
let base_path = object.base_path.clone();
(target_path, base_path, Some(project_config))
}
_ => bail!("Either target and base or project and unit must be specified"),
};
if let Some(output) = &args.output {
run_oneshot(&args, output, target_path.as_deref(), base_path.as_deref())
} else {
run_interactive(args, target_path, base_path, project_config)
}
}
fn run_oneshot(
args: &Args,
output: &Path,
target_path: Option<&Path>,
base_path: Option<&Path>,
) -> Result<()> {
let output_format = OutputFormat::from_option(args.format.as_deref())?;
let config = diff::DiffObjConfig {
relax_reloc_diffs: args.relax_reloc_diffs,
..Default::default() // TODO
};
let target = target_path
.map(|p| obj::read::read(p, &config).with_context(|| format!("Loading {}", p.display())))
.transpose()?;
let base = base_path
.map(|p| obj::read::read(p, &config).with_context(|| format!("Loading {}", p.display())))
.transpose()?;
let result = diff::diff_objs(&config, target.as_ref(), base.as_ref(), None)?;
let left = target.as_ref().and_then(|o| result.left.as_ref().map(|d| (o, d)));
let right = base.as_ref().and_then(|o| result.right.as_ref().map(|d| (o, d)));
write_output(&DiffResult::new(left, right), Some(output), output_format)?;
Ok(())
}
fn run_interactive(
args: Args,
target_path: Option<PathBuf>,
base_path: Option<PathBuf>,
project_config: Option<ProjectConfig>,
) -> Result<()> {
let Some(symbol_name) = &args.symbol else { bail!("Interactive mode requires a symbol name") };
let time_format = time::format_description::parse_borrowed::<2>("[hour]:[minute]:[second]")
.context("Failed to parse time format")?;
let mut state = Box::new(FunctionDiffUi {
relax_reloc_diffs: args.relax_reloc_diffs,
left_highlight: HighlightKind::None,
right_highlight: HighlightKind::None,
scroll_x: 0,
scroll_state_x: ScrollbarState::default(),
scroll_y: 0,
scroll_state_y: ScrollbarState::default(),
per_page: 0,
num_rows: 0,
symbol_name: symbol_name.clone(),
target_path,
base_path,
project_config,
left_obj: None,
right_obj: None,
prev_obj: None,
diff_result: DiffObjsResult::default(),
left_sym: None,
right_sym: None,
prev_sym: None,
reload_time: None,
time_format,
open_options: false,
three_way: false,
});
state.reload()?;
crossterm_panic_handler();
enable_raw_mode()?;
crossterm::queue!(
stdout(),
EnterAlternateScreen,
EnableMouseCapture,
SetTitle(format!("{} - objdiff", symbol_name)),
)?;
let backend = CrosstermBackend::new(stdout());
let mut terminal = Terminal::new(backend)?;
'outer: loop {
let mut result = EventResult { redraw: true, ..Default::default() };
loop {
if result.redraw {
terminal.draw(|f| loop {
result.redraw = false;
state.draw(f, &mut result);
if state.open_options {
state.draw_options(f, &mut result);
}
result.click_xy = None;
if !result.redraw {
break;
}
// Clear buffer on redraw
f.buffer_mut().reset();
})?;
}
match state.handle_event(event::read()?) {
EventControlFlow::Break => break 'outer,
EventControlFlow::Continue(r) => result = r,
EventControlFlow::Reload => break,
}
}
state.reload()?;
}
// Reset terminal
disable_raw_mode()?;
crossterm::execute!(stdout(), LeaveAlternateScreen, DisableMouseCapture)?;
terminal.show_cursor()?;
Ok(())
}
#[inline]
fn get_symbol(obj: Option<&ObjInfo>, sym: Option<SymbolRef>) -> Option<&ObjSymbol> {
Some(obj?.section_symbol(sym?).1)
}
#[inline]
fn get_symbol_diff(obj: Option<&ObjDiff>, sym: Option<SymbolRef>) -> Option<&ObjSymbolDiff> {
Some(obj?.symbol_diff(sym?))
}
fn find_function(obj: &ObjInfo, name: &str) -> Option<SymbolRef> {
for (section_idx, section) in obj.sections.iter().enumerate() {
if section.kind != ObjSectionKind::Code {
continue;
}
for (symbol_idx, symbol) in section.symbols.iter().enumerate() {
if symbol.name == name {
return Some(SymbolRef { section_idx, symbol_idx });
}
}
}
None
}
#[expect(dead_code)]
struct FunctionDiffUi {
relax_reloc_diffs: bool,
left_highlight: HighlightKind,
right_highlight: HighlightKind,
scroll_x: usize,
scroll_state_x: ScrollbarState,
scroll_y: usize,
scroll_state_y: ScrollbarState,
per_page: usize,
num_rows: usize,
symbol_name: String,
target_path: Option<PathBuf>,
base_path: Option<PathBuf>,
project_config: Option<ProjectConfig>,
left_obj: Option<ObjInfo>,
right_obj: Option<ObjInfo>,
prev_obj: Option<ObjInfo>,
diff_result: DiffObjsResult,
left_sym: Option<SymbolRef>,
right_sym: Option<SymbolRef>,
prev_sym: Option<SymbolRef>,
reload_time: Option<time::OffsetDateTime>,
time_format: Vec<time::format_description::FormatItem<'static>>,
open_options: bool,
three_way: bool,
}
#[derive(Default)]
struct EventResult {
redraw: bool,
click_xy: Option<(u16, u16)>,
}
enum EventControlFlow {
Break,
Continue(EventResult),
Reload,
}
impl FunctionDiffUi {
fn draw(&mut self, f: &mut Frame, result: &mut EventResult) {
let chunks = Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]).split(f.area());
let header_chunks = Layout::horizontal([
Constraint::Fill(1),
Constraint::Length(3),
Constraint::Fill(1),
Constraint::Length(2),
])
.split(chunks[0]);
let content_chunks = if self.three_way {
Layout::horizontal([
Constraint::Fill(1),
Constraint::Length(3),
Constraint::Fill(1),
Constraint::Length(3),
Constraint::Fill(1),
Constraint::Length(2),
])
.split(chunks[1])
} else {
Layout::horizontal([
Constraint::Fill(1),
Constraint::Length(3),
Constraint::Fill(1),
Constraint::Length(2),
])
.split(chunks[1])
};
self.per_page = chunks[1].height.saturating_sub(2) as usize;
let max_scroll_y = self.num_rows.saturating_sub(self.per_page);
if self.scroll_y > max_scroll_y {
self.scroll_y = max_scroll_y;
}
self.scroll_state_y =
self.scroll_state_y.content_length(max_scroll_y).position(self.scroll_y);
let mut line_l = Line::default();
line_l
.spans
.push(Span::styled(self.symbol_name.clone(), Style::new().fg(Color::White).bold()));
f.render_widget(line_l, header_chunks[0]);
let mut line_r = Line::default();
if let Some(percent) = get_symbol_diff(self.diff_result.right.as_ref(), self.right_sym)
.and_then(|s| s.match_percent)
{
line_r.spans.push(Span::styled(
format!("{:.2}% ", percent),
Style::new().fg(match_percent_color(percent)),
));
}
let reload_time = self
.reload_time
.as_ref()
.and_then(|t| t.format(&self.time_format).ok())
.unwrap_or_else(|| "N/A".to_string());
line_r.spans.push(Span::styled(
format!("Last reload: {}", reload_time),
Style::new().fg(Color::White),
));
f.render_widget(line_r, header_chunks[2]);
let mut left_text = None;
let mut left_highlight = None;
let mut max_width = 0;
if let (Some(symbol), Some(symbol_diff)) = (
get_symbol(self.left_obj.as_ref(), self.left_sym),
get_symbol_diff(self.diff_result.left.as_ref(), self.left_sym),
) {
let mut text = Text::default();
let rect = content_chunks[0].inner(Margin::new(0, 1));
left_highlight = self.print_sym(
&mut text,
symbol,
symbol_diff,
rect,
&self.left_highlight,
result,
false,
);
max_width = max_width.max(text.width());
left_text = Some(text);
}
let mut right_text = None;
let mut right_highlight = None;
let mut margin_text = None;
if let (Some(symbol), Some(symbol_diff)) = (
get_symbol(self.right_obj.as_ref(), self.right_sym),
get_symbol_diff(self.diff_result.right.as_ref(), self.right_sym),
) {
let mut text = Text::default();
let rect = content_chunks[2].inner(Margin::new(0, 1));
right_highlight = self.print_sym(
&mut text,
symbol,
symbol_diff,
rect,
&self.right_highlight,
result,
false,
);
max_width = max_width.max(text.width());
right_text = Some(text);
// Render margin
let mut text = Text::default();
let rect = content_chunks[1].inner(Margin::new(1, 1));
self.print_margin(&mut text, symbol_diff, rect);
margin_text = Some(text);
}
let mut prev_text = None;
let mut prev_margin_text = None;
if self.three_way {
if let (Some(symbol), Some(symbol_diff)) = (
get_symbol(self.prev_obj.as_ref(), self.prev_sym),
get_symbol_diff(self.diff_result.prev.as_ref(), self.prev_sym),
) {
let mut text = Text::default();
let rect = content_chunks[4].inner(Margin::new(0, 1));
self.print_sym(
&mut text,
symbol,
symbol_diff,
rect,
&self.right_highlight,
result,
true,
);
max_width = max_width.max(text.width());
prev_text = Some(text);
// Render margin
let mut text = Text::default();
let rect = content_chunks[3].inner(Margin::new(1, 1));
self.print_margin(&mut text, symbol_diff, rect);
prev_margin_text = Some(text);
}
}
let max_scroll_x =
max_width.saturating_sub(content_chunks[0].width.min(content_chunks[2].width) as usize);
if self.scroll_x > max_scroll_x {
self.scroll_x = max_scroll_x;
}
self.scroll_state_x =
self.scroll_state_x.content_length(max_scroll_x).position(self.scroll_x);
if let Some(text) = left_text {
// Render left column
f.render_widget(
Paragraph::new(text)
.block(
Block::new()
.borders(Borders::TOP)
.border_style(Style::new().fg(Color::Gray))
.title_style(Style::new().bold())
.title("TARGET"),
)
.scroll((0, self.scroll_x as u16)),
content_chunks[0],
);
}
if let Some(text) = margin_text {
f.render_widget(text, content_chunks[1].inner(Margin::new(1, 1)));
}
if let Some(text) = right_text {
f.render_widget(
Paragraph::new(text)
.block(
Block::new()
.borders(Borders::TOP)
.border_style(Style::new().fg(Color::Gray))
.title_style(Style::new().bold())
.title("CURRENT"),
)
.scroll((0, self.scroll_x as u16)),
content_chunks[2],
);
}
if self.three_way {
if let Some(text) = prev_margin_text {
f.render_widget(text, content_chunks[3].inner(Margin::new(1, 1)));
}
let block = Block::new()
.borders(Borders::TOP)
.border_style(Style::new().fg(Color::Gray))
.title_style(Style::new().bold())
.title("SAVED");
if let Some(text) = prev_text {
f.render_widget(
Paragraph::new(text).block(block.clone()).scroll((0, self.scroll_x as u16)),
content_chunks[4],
);
} else {
f.render_widget(block, content_chunks[4]);
}
}
// Render scrollbars
f.render_stateful_widget(
Scrollbar::new(ScrollbarOrientation::VerticalRight).begin_symbol(None).end_symbol(None),
chunks[1].inner(Margin::new(0, 1)),
&mut self.scroll_state_y,
);
f.render_stateful_widget(
Scrollbar::new(ScrollbarOrientation::HorizontalBottom).thumb_symbol(""),
content_chunks[0],
&mut self.scroll_state_x,
);
f.render_stateful_widget(
Scrollbar::new(ScrollbarOrientation::HorizontalBottom).thumb_symbol(""),
content_chunks[2],
&mut self.scroll_state_x,
);
if self.three_way {
f.render_stateful_widget(
Scrollbar::new(ScrollbarOrientation::HorizontalBottom).thumb_symbol(""),
content_chunks[4],
&mut self.scroll_state_x,
);
}
if let Some(new_highlight) = left_highlight {
if new_highlight == self.left_highlight {
if self.left_highlight != self.right_highlight {
self.right_highlight = self.left_highlight.clone();
} else {
self.left_highlight = HighlightKind::None;
self.right_highlight = HighlightKind::None;
}
} else {
self.left_highlight = new_highlight;
}
result.redraw = true;
} else if let Some(new_highlight) = right_highlight {
if new_highlight == self.right_highlight {
if self.left_highlight != self.right_highlight {
self.left_highlight = self.right_highlight.clone();
} else {
self.left_highlight = HighlightKind::None;
self.right_highlight = HighlightKind::None;
}
} else {
self.right_highlight = new_highlight;
}
result.redraw = true;
}
}
fn draw_options(&mut self, f: &mut Frame, _result: &mut EventResult) {
let percent_x = 50;
let percent_y = 50;
let popup_rect = Layout::vertical([
Constraint::Percentage((100 - percent_y) / 2),
Constraint::Percentage(percent_y),
Constraint::Percentage((100 - percent_y) / 2),
])
.split(f.area())[1];
let popup_rect = Layout::horizontal([
Constraint::Percentage((100 - percent_x) / 2),
Constraint::Percentage(percent_x),
Constraint::Percentage((100 - percent_x) / 2),
])
.split(popup_rect)[1];
let popup = Block::default()
.borders(Borders::ALL)
.title("Options")
.title_style(Style::default().fg(Color::White).bg(Color::Black));
f.render_widget(Clear, popup_rect);
f.render_widget(popup, popup_rect);
}
fn handle_event(&mut self, event: Event) -> EventControlFlow {
let mut result = EventResult::default();
match event {
Event::Key(event)
if matches!(event.kind, KeyEventKind::Press | KeyEventKind::Repeat) =>
{
match event.code {
// Quit
KeyCode::Esc | KeyCode::Char('q') => return EventControlFlow::Break,
// Page up
KeyCode::PageUp => {
self.page_up(false);
result.redraw = true;
}
// Page up (shift + space)
KeyCode::Char(' ') if event.modifiers.contains(KeyModifiers::SHIFT) => {
self.page_up(false);
result.redraw = true;
}
// Page down
KeyCode::Char(' ') | KeyCode::PageDown => {
self.page_down(false);
result.redraw = true;
}
// Page down (ctrl + f)
KeyCode::Char('f') if event.modifiers.contains(KeyModifiers::CONTROL) => {
self.page_down(false);
result.redraw = true;
}
// Page up (ctrl + b)
KeyCode::Char('b') if event.modifiers.contains(KeyModifiers::CONTROL) => {
self.page_up(false);
result.redraw = true;
}
// Half page down (ctrl + d)
KeyCode::Char('d') if event.modifiers.contains(KeyModifiers::CONTROL) => {
self.page_down(true);
result.redraw = true;
}
// Half page up (ctrl + u)
KeyCode::Char('u') if event.modifiers.contains(KeyModifiers::CONTROL) => {
self.page_up(true);
result.redraw = true;
}
// Scroll down
KeyCode::Down | KeyCode::Char('j') => {
self.scroll_y += 1;
result.redraw = true;
}
// Scroll up
KeyCode::Up | KeyCode::Char('k') => {
self.scroll_y = self.scroll_y.saturating_sub(1);
result.redraw = true;
}
// Scroll to start
KeyCode::Char('g') => {
self.scroll_y = 0;
result.redraw = true;
}
// Scroll to end
KeyCode::Char('G') => {
self.scroll_y = self.num_rows;
result.redraw = true;
}
// Reload
KeyCode::Char('r') => {
result.redraw = true;
return EventControlFlow::Reload;
}
// Scroll right
KeyCode::Right | KeyCode::Char('l') => {
self.scroll_x += 1;
result.redraw = true;
}
// Scroll left
KeyCode::Left | KeyCode::Char('h') => {
self.scroll_x = self.scroll_x.saturating_sub(1);
result.redraw = true;
}
// Toggle relax relocation diffs
KeyCode::Char('x') => {
self.relax_reloc_diffs = !self.relax_reloc_diffs;
result.redraw = true;
return EventControlFlow::Reload;
}
// Toggle three-way diff
KeyCode::Char('3') => {
self.three_way = !self.three_way;
result.redraw = true;
}
// Toggle options
KeyCode::Char('o') => {
self.open_options = !self.open_options;
result.redraw = true;
}
_ => {}
}
}
Event::Mouse(event) => match event.kind {
MouseEventKind::ScrollDown => {
self.scroll_y += 3;
result.redraw = true;
}
MouseEventKind::ScrollUp => {
self.scroll_y = self.scroll_y.saturating_sub(3);
result.redraw = true;
}
MouseEventKind::ScrollRight => {
self.scroll_x += 3;
result.redraw = true;
}
MouseEventKind::ScrollLeft => {
self.scroll_x = self.scroll_x.saturating_sub(3);
result.redraw = true;
}
MouseEventKind::Down(MouseButton::Left) => {
result.click_xy = Some((event.column, event.row));
result.redraw = true;
}
_ => {}
},
Event::Resize(_, _) => {
result.redraw = true;
}
_ => {}
}
EventControlFlow::Continue(result)
}
fn page_up(&mut self, half: bool) {
self.scroll_y = self.scroll_y.saturating_sub(self.per_page / if half { 2 } else { 1 });
}
fn page_down(&mut self, half: bool) {
self.scroll_y += self.per_page / if half { 2 } else { 1 };
}
#[expect(clippy::too_many_arguments)]
fn print_sym(
&self,
out: &mut Text<'static>,
symbol: &ObjSymbol,
symbol_diff: &ObjSymbolDiff,
rect: Rect,
highlight: &HighlightKind,
result: &EventResult,
only_changed: bool,
) -> Option<HighlightKind> {
let base_addr = symbol.address;
let mut new_highlight = None;
for (y, ins_diff) in symbol_diff
.instructions
.iter()
.skip(self.scroll_y)
.take(rect.height as usize)
.enumerate()
{
if only_changed && ins_diff.kind == ObjInsDiffKind::None {
out.lines.push(Line::default());
continue;
}
let mut sx = rect.x;
let sy = rect.y + y as u16;
let mut line = Line::default();
display_diff(ins_diff, base_addr, |text| -> Result<()> {
let label_text;
let mut base_color = match ins_diff.kind {
ObjInsDiffKind::None
| ObjInsDiffKind::OpMismatch
| ObjInsDiffKind::ArgMismatch => Color::Gray,
ObjInsDiffKind::Replace => Color::Cyan,
ObjInsDiffKind::Delete => Color::Red,
ObjInsDiffKind::Insert => Color::Green,
};
let mut pad_to = 0;
match text {
DiffText::Basic(text) => {
label_text = text.to_string();
}
DiffText::BasicColor(s, idx) => {
label_text = s.to_string();
base_color = COLOR_ROTATION[idx % COLOR_ROTATION.len()];
}
DiffText::Line(num) => {
label_text = format!("{num} ");
base_color = Color::DarkGray;
pad_to = 5;
}
DiffText::Address(addr) => {
label_text = format!("{:x}:", addr);
pad_to = 5;
}
DiffText::Opcode(mnemonic, _op) => {
label_text = mnemonic.to_string();
if ins_diff.kind == ObjInsDiffKind::OpMismatch {
base_color = Color::Blue;
}
pad_to = 8;
}
DiffText::Argument(arg, diff) => {
label_text = arg.to_string();
if let Some(diff) = diff {
base_color = COLOR_ROTATION[diff.idx % COLOR_ROTATION.len()]
}
}
DiffText::BranchDest(addr, diff) => {
label_text = format!("{addr:x}");
if let Some(diff) = diff {
base_color = COLOR_ROTATION[diff.idx % COLOR_ROTATION.len()]
}
}
DiffText::Symbol(sym, diff) => {
let name = sym.demangled_name.as_ref().unwrap_or(&sym.name);
label_text = name.clone();
if let Some(diff) = diff {
base_color = COLOR_ROTATION[diff.idx % COLOR_ROTATION.len()]
} else {
base_color = Color::White;
}
}
DiffText::Spacing(n) => {
line.spans.push(Span::raw(" ".repeat(n)));
sx += n as u16;
return Ok(());
}
DiffText::Eol => {
return Ok(());
}
}
let len = label_text.len();
let highlighted = *highlight == text;
if let Some((cx, cy)) = result.click_xy {
if cx >= sx && cx < sx + len as u16 && cy == sy {
new_highlight = Some(text.into());
}
}
let mut style = Style::new().fg(base_color);
if highlighted {
style = style.bg(Color::DarkGray);
}
line.spans.push(Span::styled(label_text, style));
sx += len as u16;
if pad_to > len {
let pad = (pad_to - len) as u16;
line.spans.push(Span::raw(" ".repeat(pad as usize)));
sx += pad;
}
Ok(())
})
.unwrap();
out.lines.push(line);
}
new_highlight
}
fn print_margin(&self, out: &mut Text, symbol: &ObjSymbolDiff, rect: Rect) {
for ins_diff in symbol.instructions.iter().skip(self.scroll_y).take(rect.height as usize) {
if ins_diff.kind != ObjInsDiffKind::None {
out.lines.push(Line::raw(match ins_diff.kind {
ObjInsDiffKind::Delete => "<",
ObjInsDiffKind::Insert => ">",
_ => "|",
}));
} else {
out.lines.push(Line::raw(" "));
}
}
}
fn reload(&mut self) -> Result<()> {
let prev = self.right_obj.take();
let config = diff::DiffObjConfig {
relax_reloc_diffs: self.relax_reloc_diffs,
..Default::default() // TODO
};
let target = self
.target_path
.as_deref()
.map(|p| {
obj::read::read(p, &config).with_context(|| format!("Loading {}", p.display()))
})
.transpose()?;
let base = self
.base_path
.as_deref()
.map(|p| {
obj::read::read(p, &config).with_context(|| format!("Loading {}", p.display()))
})
.transpose()?;
let result = diff::diff_objs(&config, target.as_ref(), base.as_ref(), prev.as_ref())?;
let left_sym = target.as_ref().and_then(|o| find_function(o, &self.symbol_name));
let right_sym = base.as_ref().and_then(|o| find_function(o, &self.symbol_name));
let prev_sym = prev.as_ref().and_then(|o| find_function(o, &self.symbol_name));
self.num_rows = match (
get_symbol_diff(result.left.as_ref(), left_sym),
get_symbol_diff(result.right.as_ref(), right_sym),
) {
(Some(l), Some(r)) => l.instructions.len().max(r.instructions.len()),
(Some(l), None) => l.instructions.len(),
(None, Some(r)) => r.instructions.len(),
(None, None) => bail!("Symbol not found: {}", self.symbol_name),
};
self.left_obj = target;
self.right_obj = base;
self.prev_obj = prev;
self.diff_result = result;
self.left_sym = left_sym;
self.right_sym = right_sym;
self.prev_sym = prev_sym;
self.reload_time = time::OffsetDateTime::now_local().ok();
Ok(())
}
}
pub const COLOR_ROTATION: [Color; 7] = [
Color::Magenta,
Color::Cyan,
Color::Green,
Color::Red,
Color::Yellow,
Color::Blue,
Color::Green,
];
pub fn match_percent_color(match_percent: f32) -> Color {
if match_percent == 100.0 {
Color::Green
} else if match_percent >= 50.0 {
Color::LightBlue
} else {
Color::LightRed
}
}

View File

@@ -0,0 +1,2 @@
pub mod diff;
pub mod report;

View File

@@ -0,0 +1,426 @@
use std::{
collections::HashSet,
fs::File,
io::Read,
path::{Path, PathBuf},
time::Instant,
};
use anyhow::{bail, Context, Result};
use argp::FromArgs;
use objdiff_core::{
bindings::report::{
ChangeItem, ChangeItemInfo, ChangeUnit, Changes, ChangesInput, Measures, Report,
ReportCategory, ReportItem, ReportItemMetadata, ReportUnit, ReportUnitMetadata,
REPORT_VERSION,
},
config::ProjectObject,
diff, obj,
obj::{ObjSectionKind, ObjSymbolFlags},
};
use prost::Message;
use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator};
use tracing::{info, warn};
use crate::util::output::{write_output, OutputFormat};
#[derive(FromArgs, PartialEq, Debug)]
/// Generate a progress report for a project.
#[argp(subcommand, name = "report")]
pub struct Args {
#[argp(subcommand)]
command: SubCommand,
}
#[derive(FromArgs, PartialEq, Debug)]
#[argp(subcommand)]
pub enum SubCommand {
Generate(GenerateArgs),
Changes(ChangesArgs),
}
#[derive(FromArgs, PartialEq, Debug)]
/// Generate a progress report for a project.
#[argp(subcommand, name = "generate")]
pub struct GenerateArgs {
#[argp(option, short = 'p')]
/// Project directory
project: Option<PathBuf>,
#[argp(option, short = 'o')]
/// Output file
output: Option<PathBuf>,
#[argp(switch, short = 'd')]
/// Deduplicate global and weak symbols (runs single-threaded)
deduplicate: bool,
#[argp(option, short = 'f')]
/// Output format (json, json-pretty, proto) (default: json)
format: Option<String>,
}
#[derive(FromArgs, PartialEq, Debug)]
/// List any changes from a previous report.
#[argp(subcommand, name = "changes")]
pub struct ChangesArgs {
#[argp(positional)]
/// Previous report file
previous: PathBuf,
#[argp(positional)]
/// Current report file
current: PathBuf,
#[argp(option, short = 'o')]
/// Output file
output: Option<PathBuf>,
#[argp(option, short = 'f')]
/// Output format (json, json-pretty, proto) (default: json)
format: Option<String>,
}
pub fn run(args: Args) -> Result<()> {
match args.command {
SubCommand::Generate(args) => generate(args),
SubCommand::Changes(args) => changes(args),
}
}
fn generate(args: GenerateArgs) -> Result<()> {
let output_format = OutputFormat::from_option(args.format.as_deref())?;
let project_dir = args.project.as_deref().unwrap_or_else(|| Path::new("."));
info!("Loading project {}", project_dir.display());
let mut project = match objdiff_core::config::try_project_config(project_dir) {
Some((Ok(config), _)) => config,
Some((Err(err), _)) => bail!("Failed to load project configuration: {}", err),
None => bail!("No project configuration found"),
};
info!(
"Generating report for {} units (using {} threads)",
project.units().len(),
if args.deduplicate { 1 } else { rayon::current_num_threads() }
);
let start = Instant::now();
let mut units = vec![];
let mut existing_functions: HashSet<String> = HashSet::new();
if args.deduplicate {
// If deduplicating, we need to run single-threaded
for object in project.units.as_deref_mut().unwrap_or_default() {
if let Some(unit) = report_object(
object,
project_dir,
project.target_dir.as_deref(),
project.base_dir.as_deref(),
Some(&mut existing_functions),
)? {
units.push(unit);
}
}
} else {
let vec = project
.units
.as_deref_mut()
.unwrap_or_default()
.par_iter_mut()
.map(|object| {
report_object(
object,
project_dir,
project.target_dir.as_deref(),
project.base_dir.as_deref(),
None,
)
})
.collect::<Result<Vec<Option<ReportUnit>>>>()?;
units = vec.into_iter().flatten().collect();
}
let measures = units.iter().flat_map(|u| u.measures.into_iter()).collect();
let mut categories = Vec::new();
for category in project.progress_categories() {
categories.push(ReportCategory {
id: category.id.clone(),
name: category.name.clone(),
measures: Some(Default::default()),
});
}
let mut report =
Report { measures: Some(measures), units, version: REPORT_VERSION, categories };
report.calculate_progress_categories();
let duration = start.elapsed();
info!("Report generated in {}.{:03}s", duration.as_secs(), duration.subsec_millis());
write_output(&report, args.output.as_deref(), output_format)?;
Ok(())
}
fn report_object(
object: &mut ProjectObject,
project_dir: &Path,
target_dir: Option<&Path>,
base_dir: Option<&Path>,
mut existing_functions: Option<&mut HashSet<String>>,
) -> Result<Option<ReportUnit>> {
object.resolve_paths(project_dir, target_dir, base_dir);
match (&object.target_path, &object.base_path) {
(None, Some(_)) if !object.complete().unwrap_or(false) => {
warn!("Skipping object without target: {}", object.name());
return Ok(None);
}
(None, None) => {
warn!("Skipping object without target or base: {}", object.name());
return Ok(None);
}
_ => {}
}
let config = diff::DiffObjConfig { relax_reloc_diffs: true, ..Default::default() };
let target = object
.target_path
.as_ref()
.map(|p| {
obj::read::read(p, &config).with_context(|| format!("Failed to open {}", p.display()))
})
.transpose()?;
let base = object
.base_path
.as_ref()
.map(|p| {
obj::read::read(p, &config).with_context(|| format!("Failed to open {}", p.display()))
})
.transpose()?;
let result = diff::diff_objs(&config, target.as_ref(), base.as_ref(), None)?;
let metadata = ReportUnitMetadata {
complete: object.complete(),
module_name: target
.as_ref()
.and_then(|o| o.split_meta.as_ref())
.and_then(|m| m.module_name.clone()),
module_id: target.as_ref().and_then(|o| o.split_meta.as_ref()).and_then(|m| m.module_id),
source_path: object.metadata.as_ref().and_then(|m| m.source_path.clone()),
progress_categories: object
.metadata
.as_ref()
.and_then(|m| m.progress_categories.clone())
.unwrap_or_default(),
auto_generated: object.metadata.as_ref().and_then(|m| m.auto_generated),
};
let mut measures = Measures { total_units: 1, ..Default::default() };
let mut sections = vec![];
let mut functions = vec![];
let obj = target.as_ref().or(base.as_ref()).unwrap();
let obj_diff = result.left.as_ref().or(result.right.as_ref()).unwrap();
for (section, section_diff) in obj.sections.iter().zip(&obj_diff.sections) {
let section_match_percent = section_diff.match_percent.unwrap_or_else(|| {
// Support cases where we don't have a target object,
// assume complete means 100% match
if object.complete().unwrap_or(false) {
100.0
} else {
0.0
}
});
sections.push(ReportItem {
name: section.name.clone(),
fuzzy_match_percent: section_match_percent,
size: section.size,
metadata: Some(ReportItemMetadata {
demangled_name: None,
virtual_address: section.virtual_address,
}),
});
match section.kind {
ObjSectionKind::Data | ObjSectionKind::Bss => {
measures.total_data += section.size;
if section_match_percent == 100.0 {
measures.matched_data += section.size;
}
continue;
}
ObjSectionKind::Code => (),
}
for (symbol, symbol_diff) in section.symbols.iter().zip(&section_diff.symbols) {
if symbol.size == 0 || symbol.flags.0.contains(ObjSymbolFlags::Hidden) {
continue;
}
if let Some(existing_functions) = &mut existing_functions {
if (symbol.flags.0.contains(ObjSymbolFlags::Global)
|| symbol.flags.0.contains(ObjSymbolFlags::Weak))
&& !existing_functions.insert(symbol.name.clone())
{
continue;
}
}
let match_percent = symbol_diff.match_percent.unwrap_or_else(|| {
// Support cases where we don't have a target object,
// assume complete means 100% match
if object.complete().unwrap_or(false) {
100.0
} else {
0.0
}
});
measures.fuzzy_match_percent += match_percent * symbol.size as f32;
measures.total_code += symbol.size;
if match_percent == 100.0 {
measures.matched_code += symbol.size;
}
functions.push(ReportItem {
name: symbol.name.clone(),
size: symbol.size,
fuzzy_match_percent: match_percent,
metadata: Some(ReportItemMetadata {
demangled_name: symbol.demangled_name.clone(),
virtual_address: symbol.virtual_address,
}),
});
if match_percent == 100.0 {
measures.matched_functions += 1;
}
measures.total_functions += 1;
}
}
if metadata.complete.unwrap_or(false) {
measures.complete_code = measures.total_code;
measures.complete_data = measures.total_data;
measures.complete_units = 1;
}
measures.calc_fuzzy_match_percent();
measures.calc_matched_percent();
Ok(Some(ReportUnit {
name: object.name().to_string(),
measures: Some(measures),
sections,
functions,
metadata: Some(metadata),
}))
}
fn changes(args: ChangesArgs) -> Result<()> {
let output_format = OutputFormat::from_option(args.format.as_deref())?;
let (previous, current) = if args.previous == Path::new("-") && args.current == Path::new("-") {
// Special case for comparing two reports from stdin
let mut data = vec![];
std::io::stdin().read_to_end(&mut data)?;
let input = ChangesInput::decode(data.as_slice())?;
(input.from.unwrap(), input.to.unwrap())
} else {
let previous = read_report(&args.previous)?;
let current = read_report(&args.current)?;
(previous, current)
};
let mut changes = Changes { from: previous.measures, to: current.measures, units: vec![] };
for prev_unit in &previous.units {
let curr_unit = current.units.iter().find(|u| u.name == prev_unit.name);
let sections = process_items(prev_unit, curr_unit, |u| &u.sections);
let functions = process_items(prev_unit, curr_unit, |u| &u.functions);
let prev_measures = prev_unit.measures;
let curr_measures = curr_unit.and_then(|u| u.measures);
if !functions.is_empty() || prev_measures != curr_measures {
changes.units.push(ChangeUnit {
name: prev_unit.name.clone(),
from: prev_measures,
to: curr_measures,
sections,
functions,
metadata: curr_unit
.as_ref()
.and_then(|u| u.metadata.clone())
.or_else(|| prev_unit.metadata.clone()),
});
}
}
for curr_unit in &current.units {
if !previous.units.iter().any(|u| u.name == curr_unit.name) {
changes.units.push(ChangeUnit {
name: curr_unit.name.clone(),
from: None,
to: curr_unit.measures,
sections: process_new_items(&curr_unit.sections),
functions: process_new_items(&curr_unit.functions),
metadata: curr_unit.metadata.clone(),
});
}
}
write_output(&changes, args.output.as_deref(), output_format)?;
Ok(())
}
fn process_items<F: Fn(&ReportUnit) -> &Vec<ReportItem>>(
prev_unit: &ReportUnit,
curr_unit: Option<&ReportUnit>,
getter: F,
) -> Vec<ChangeItem> {
let prev_items = getter(prev_unit);
let mut items = vec![];
if let Some(curr_unit) = curr_unit {
let curr_items = getter(curr_unit);
for prev_func in prev_items {
let prev_func_info = ChangeItemInfo::from(prev_func);
let curr_func = curr_items.iter().find(|f| f.name == prev_func.name);
let curr_func_info = curr_func.map(ChangeItemInfo::from);
if let Some(curr_func_info) = curr_func_info {
if prev_func_info != curr_func_info {
items.push(ChangeItem {
name: prev_func.name.clone(),
from: Some(prev_func_info),
to: Some(curr_func_info),
metadata: curr_func.as_ref().unwrap().metadata.clone(),
});
}
} else {
items.push(ChangeItem {
name: prev_func.name.clone(),
from: Some(prev_func_info),
to: None,
metadata: prev_func.metadata.clone(),
});
}
}
for curr_func in curr_items {
if !prev_items.iter().any(|f| f.name == curr_func.name) {
items.push(ChangeItem {
name: curr_func.name.clone(),
from: None,
to: Some(ChangeItemInfo::from(curr_func)),
metadata: curr_func.metadata.clone(),
});
}
}
} else {
for prev_func in prev_items {
items.push(ChangeItem {
name: prev_func.name.clone(),
from: Some(ChangeItemInfo::from(prev_func)),
to: None,
metadata: prev_func.metadata.clone(),
});
}
}
items
}
fn process_new_items(items: &[ReportItem]) -> Vec<ChangeItem> {
items
.iter()
.map(|item| ChangeItem {
name: item.name.clone(),
from: None,
to: Some(ChangeItemInfo::from(item)),
metadata: item.metadata.clone(),
})
.collect()
}
fn read_report(path: &Path) -> Result<Report> {
if path == Path::new("-") {
let mut data = vec![];
std::io::stdin().read_to_end(&mut data)?;
return Report::parse(&data).with_context(|| "Failed to load report from stdin");
}
let file = File::open(path).with_context(|| format!("Failed to open {}", path.display()))?;
let mmap = unsafe { memmap2::Mmap::map(&file) }
.with_context(|| format!("Failed to map {}", path.display()))?;
Report::parse(mmap.as_ref())
.with_context(|| format!("Failed to load report {}", path.display()))
}

147
objdiff-cli/src/main.rs Normal file
View File

@@ -0,0 +1,147 @@
mod argp_version;
mod cmd;
mod util;
// musl's allocator is very slow, so use mimalloc when targeting musl.
// Otherwise, use the system allocator to avoid extra code size.
#[cfg(target_env = "musl")]
#[global_allocator]
static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc;
use std::{env, ffi::OsStr, fmt::Display, path::PathBuf, str::FromStr};
use anyhow::{Error, Result};
use argp::{FromArgValue, FromArgs};
use enable_ansi_support::enable_ansi_support;
use supports_color::Stream;
use tracing_subscriber::{filter::LevelFilter, EnvFilter};
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
enum LogLevel {
Error,
Warn,
Info,
Debug,
Trace,
}
impl FromStr for LogLevel {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s {
"error" => Self::Error,
"warn" => Self::Warn,
"info" => Self::Info,
"debug" => Self::Debug,
"trace" => Self::Trace,
_ => return Err(()),
})
}
}
impl Display for LogLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
LogLevel::Error => "error",
LogLevel::Warn => "warn",
LogLevel::Info => "info",
LogLevel::Debug => "debug",
LogLevel::Trace => "trace",
})
}
}
impl FromArgValue for LogLevel {
fn from_arg_value(value: &OsStr) -> Result<Self, String> {
String::from_arg_value(value)
.and_then(|s| Self::from_str(&s).map_err(|_| "Invalid log level".to_string()))
}
}
#[derive(FromArgs, PartialEq, Debug)]
/// A local diffing tool for decompilation projects.
struct TopLevel {
#[argp(subcommand)]
command: SubCommand,
#[argp(option, short = 'C')]
/// Change working directory.
chdir: Option<PathBuf>,
#[argp(option, short = 'L')]
/// Minimum logging level. (Default: info)
/// Possible values: error, warn, info, debug, trace
log_level: Option<LogLevel>,
/// Print version information and exit.
#[argp(switch, short = 'V')]
version: bool,
/// Disable color output. (env: NO_COLOR)
#[argp(switch)]
no_color: bool,
}
#[derive(FromArgs, PartialEq, Debug)]
#[argp(subcommand)]
enum SubCommand {
Diff(cmd::diff::Args),
Report(cmd::report::Args),
}
// Duplicated from supports-color so we can check early.
fn env_no_color() -> bool {
match env::var("NO_COLOR").as_deref() {
Ok("") | Ok("0") | Err(_) => false,
Ok(_) => true,
}
}
fn main() {
let args: TopLevel = argp_version::from_env();
let use_colors = if args.no_color || env_no_color() {
false
} else {
// Try to enable ANSI support on Windows.
let _ = enable_ansi_support();
// Disable isatty check for supports-color. (e.g. when used with ninja)
unsafe { env::set_var("IGNORE_IS_TERMINAL", "1") };
supports_color::on(Stream::Stdout).is_some_and(|c| c.has_basic)
};
let format =
tracing_subscriber::fmt::format().with_ansi(use_colors).with_target(false).without_time();
let builder = tracing_subscriber::fmt().event_format(format).with_writer(std::io::stderr);
if let Some(level) = args.log_level {
builder
.with_max_level(match level {
LogLevel::Error => LevelFilter::ERROR,
LogLevel::Warn => LevelFilter::WARN,
LogLevel::Info => LevelFilter::INFO,
LogLevel::Debug => LevelFilter::DEBUG,
LogLevel::Trace => LevelFilter::TRACE,
})
.init();
} else {
builder
.with_env_filter(
EnvFilter::builder()
.with_default_directive(LevelFilter::INFO.into())
.from_env_lossy(),
)
.init();
}
let mut result = Ok(());
if let Some(dir) = &args.chdir {
result = env::set_current_dir(dir).map_err(|e| {
Error::new(e)
.context(format!("Failed to change working directory to '{}'", dir.display()))
});
}
result = result.and_then(|_| match args.command {
SubCommand::Diff(c_args) => cmd::diff::run(c_args),
SubCommand::Report(c_args) => cmd::report::run(c_args),
});
if let Err(e) = result {
eprintln!("Failed: {e:?}");
std::process::exit(1);
}
}

View File

@@ -0,0 +1,2 @@
pub mod output;
pub mod term;

View File

@@ -0,0 +1,84 @@
use std::{
fs::File,
io::{BufWriter, Write},
ops::DerefMut,
path::Path,
};
use anyhow::{bail, Context, Result};
use tracing::info;
#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
pub enum OutputFormat {
#[default]
Json,
JsonPretty,
Proto,
}
impl OutputFormat {
pub fn from_str(s: &str) -> Result<Self> {
match s.to_ascii_lowercase().as_str() {
"json" => Ok(Self::Json),
"json-pretty" | "json_pretty" => Ok(Self::JsonPretty),
"binpb" | "pb" | "proto" | "protobuf" => Ok(Self::Proto),
_ => bail!("Invalid output format: {}", s),
}
}
pub fn from_option(s: Option<&str>) -> Result<Self> {
match s {
Some(s) => Self::from_str(s),
None => Ok(Self::default()),
}
}
}
pub fn write_output<T>(input: &T, output: Option<&Path>, format: OutputFormat) -> Result<()>
where T: serde::Serialize + prost::Message {
match output {
Some(output) if output != Path::new("-") => {
info!("Writing to {}", output.display());
let file = File::options()
.read(true)
.write(true)
.create(true)
.truncate(true)
.open(output)
.with_context(|| format!("Failed to create file {}", output.display()))?;
match format {
OutputFormat::Json => {
let mut output = BufWriter::new(file);
serde_json::to_writer(&mut output, input)
.context("Failed to write output file")?;
output.flush().context("Failed to flush output file")?;
}
OutputFormat::JsonPretty => {
let mut output = BufWriter::new(file);
serde_json::to_writer_pretty(&mut output, input)
.context("Failed to write output file")?;
output.flush().context("Failed to flush output file")?;
}
OutputFormat::Proto => {
file.set_len(input.encoded_len() as u64)?;
let map = unsafe { memmap2::Mmap::map(&file) }
.context("Failed to map output file")?;
let mut output = map.make_mut().context("Failed to remap output file")?;
input.encode(&mut output.deref_mut()).context("Failed to encode output")?;
}
}
}
_ => match format {
OutputFormat::Json => {
serde_json::to_writer(std::io::stdout(), input)?;
}
OutputFormat::JsonPretty => {
serde_json::to_writer_pretty(std::io::stdout(), input)?;
}
OutputFormat::Proto => {
std::io::stdout().write_all(&input.encode_to_vec())?;
}
},
}
Ok(())
}

View File

@@ -0,0 +1,16 @@
use std::{io::stdout, panic};
use crossterm::{
cursor::Show,
event::DisableMouseCapture,
terminal::{disable_raw_mode, LeaveAlternateScreen},
};
pub fn crossterm_panic_handler() {
let original_hook = panic::take_hook();
panic::set_hook(Box::new(move |panic_info| {
let _ = crossterm::execute!(stdout(), LeaveAlternateScreen, DisableMouseCapture, Show);
let _ = disable_raw_mode();
original_hook(panic_info);
}));
}

86
objdiff-core/Cargo.toml Normal file
View File

@@ -0,0 +1,86 @@
[package]
name = "objdiff-core"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
readme = "README.md"
description = """
A local diffing tool for decompilation projects.
"""
documentation = "https://docs.rs/objdiff-core"
[lib]
crate-type = ["cdylib", "rlib"]
[features]
all = ["config", "dwarf", "mips", "ppc", "x86", "arm", "arm64", "bindings"]
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 = ["dep:bimap", "dep:globset", "dep:semver", "dep:serde_json", "dep:serde_yaml", "dep:serde", "dep:filetime"]
dwarf = ["dep:gimli"]
mips = ["any-arch", "dep:rabbitizer"]
ppc = ["any-arch", "dep:cwdemangle", "dep:cwextab", "dep:ppc750cl"]
x86 = ["any-arch", "dep:cpp_demangle", "dep:iced-x86", "dep:msvc-demangler"]
arm = ["any-arch", "dep:cpp_demangle", "dep:unarm", "dep:arm-attr"]
arm64 = ["any-arch", "dep:cpp_demangle", "dep:yaxpeax-arch", "dep:yaxpeax-arm"]
bindings = ["dep:serde_json", "dep:prost", "dep:pbjson", "dep:serde", "dep:prost-build", "dep:pbjson-build"]
wasm = ["bindings", "any-arch", "dep:console_error_panic_hook", "dep:console_log", "dep:wasm-bindgen", "dep:tsify-next", "dep:log"]
[package.metadata.docs.rs]
features = ["all"]
[dependencies]
anyhow = "1.0"
bimap = { version = "0.6", features = ["serde"], optional = true }
byteorder = { version = "1.5", optional = true }
filetime = { version = "0.2", optional = true }
flagset = { version = "0.4", optional = true }
log = { version = "0.4", optional = true }
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 }
pbjson = { version = "0.7", optional = true }
prost = { version = "0.13", optional = true }
serde = { version = "1.0", features = ["derive"], optional = true }
similar = { version = "2.6", default-features = false, optional = true }
strum = { version = "0.26", features = ["derive"], optional = true }
wasm-bindgen = { version = "0.2", optional = true }
tsify-next = { version = "0.5", default-features = false, features = ["js"], optional = true }
console_log = { version = "1.0", optional = true }
console_error_panic_hook = { version = "0.1", optional = true }
# config
globset = { version = "0.4", features = ["serde1"], optional = true }
semver = { version = "1.0", optional = true }
serde_json = { version = "1.0", optional = true }
serde_yaml = { version = "0.9", optional = true }
# dwarf
gimli = { version = "0.31", default-features = false, features = ["read-all"], optional = true }
# ppc
cwdemangle = { version = "1.0", optional = true }
cwextab = { version = "1.0.2", optional = true }
ppc750cl = { version = "0.3", optional = true }
# mips
rabbitizer = { version = "1.12", optional = true }
# x86
cpp_demangle = { version = "0.4", optional = true }
iced-x86 = { version = "1.21", default-features = false, features = ["std", "decoder", "intel", "gas", "masm", "nasm", "exhaustive_enums"], optional = true }
msvc-demangler = { version = "0.10", optional = true }
# arm
unarm = { version = "1.6", optional = true }
arm-attr = { version = "0.1", optional = true }
# arm64
yaxpeax-arch = { version = "0.3", default-features = false, features = ["std"], optional = true }
yaxpeax-arm = { version = "0.3", default-features = false, features = ["std"], optional = true }
[build-dependencies]
prost-build = { version = "0.13", optional = true }
pbjson-build = { version = "0.7", optional = true }

15
objdiff-core/README.md Normal file
View File

@@ -0,0 +1,15 @@
# objdiff-core
objdiff-core contains the core functionality of [objdiff](https://github.com/encounter/objdiff), a tool for comparing object files in decompilation projects. See the main repository for more information.
## Crate feature flags
- **`all`**: Enables all main features.
- **`config`**: Enables objdiff configuration file support.
- **`dwarf`**: Enables extraction of line number information from DWARF debug sections.
- **`mips`**: Enables the MIPS backend powered by [rabbitizer](https://github.com/Decompollaborate/rabbitizer). (Note: C library with Rust bindings)
- **`ppc`**: Enables the PowerPC backend powered by [ppc750cl](https://github.com/encounter/ppc750cl).
- **`x86`**: Enables the x86 backend powered by [iced-x86](https://crates.io/crates/iced-x86).
- **`arm`**: Enables the ARM backend powered by [unarm](https://github.com/AetiasHax/unarm).
- **`arm64`**: Enables the ARM64 backend powered by [yaxpeax-arm](https://github.com/iximeow/yaxpeax-arm).
- **`bindings`**: Enables serialization and deserialization of objdiff data structures.

59
objdiff-core/build.rs Normal file
View File

@@ -0,0 +1,59 @@
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 descriptor_path = root.join("proto_descriptor.bin");
println!("cargo:rerun-if-changed={}", descriptor_path.display());
let descriptor_mtime = std::fs::metadata(&descriptor_path)
.map(|m| m.modified().unwrap())
.unwrap_or(std::time::SystemTime::UNIX_EPOCH);
let mut run_protoc = false;
let proto_files = vec![root.join("diff.proto"), root.join("report.proto")];
for proto_file in &proto_files {
println!("cargo:rerun-if-changed={}", proto_file.display());
let mtime = match std::fs::metadata(proto_file) {
Ok(m) => m.modified().unwrap(),
Err(e) => panic!("Failed to stat proto file {}: {:?}", proto_file.display(), e),
};
if mtime > descriptor_mtime {
run_protoc = true;
}
}
fn prost_config(descriptor_path: &Path, run_protoc: bool) -> prost_build::Config {
let mut config = prost_build::Config::new();
config.file_descriptor_set_path(descriptor_path);
// If our cached descriptor is up-to-date, we don't need to run protoc.
// This is helpful so that users don't need to have protoc installed
// unless they're updating the protos.
if !run_protoc {
config.skip_protoc_run();
}
config
}
if let Err(e) =
prost_config(&descriptor_path, run_protoc).compile_protos(&proto_files, &[root.as_path()])
{
if e.kind() == std::io::ErrorKind::NotFound && e.to_string().contains("protoc") {
eprintln!("protoc not found, skipping protobuf compilation");
prost_config(&descriptor_path, false)
.compile_protos(&proto_files, &[root.as_path()])
.expect("Failed to compile protos");
} else {
panic!("Failed to compile protos: {e:?}");
}
}
let descriptor_set = std::fs::read(descriptor_path).expect("Failed to read descriptor set");
pbjson_build::Builder::new()
.register_descriptors(&descriptor_set)
.expect("Failed to register descriptors")
.preserve_proto_field_names()
.build(&[".objdiff"])
.expect("Failed to build pbjson");
}

View File

@@ -0,0 +1,163 @@
syntax = "proto3";
package objdiff.diff;
// A symbol
message Symbol {
// Name of the symbol
string name = 1;
// Demangled name of the symbol
optional string demangled_name = 2;
// Symbol address
uint64 address = 3;
// Symbol size
uint64 size = 4;
// Bitmask of SymbolFlag
uint32 flags = 5;
}
// Symbol visibility flags
enum SymbolFlag {
SYMBOL_NONE = 0;
SYMBOL_GLOBAL = 1;
SYMBOL_LOCAL = 2;
SYMBOL_WEAK = 3;
SYMBOL_COMMON = 4;
SYMBOL_HIDDEN = 5;
}
// A single parsed instruction
message Instruction {
// Instruction address
uint64 address = 1;
// Instruction size
uint32 size = 2;
// Instruction opcode
uint32 opcode = 3;
// Instruction mnemonic
string mnemonic = 4;
// Instruction formatted string
string formatted = 5;
// Original (unsimplified) instruction string
optional string original = 6;
// Instruction arguments
repeated Argument arguments = 7;
// Instruction relocation
optional Relocation relocation = 8;
// Instruction branch destination
optional uint64 branch_dest = 9;
// Instruction line number
optional uint32 line_number = 10;
}
// An instruction argument
message Argument {
oneof value {
// Plain text
string plain_text = 1;
// Value
ArgumentValue argument = 2;
// Relocation
ArgumentRelocation relocation = 3;
// Branch destination
uint64 branch_dest = 4;
}
}
// An instruction argument value
message ArgumentValue {
oneof value {
// Signed integer
int64 signed = 1;
// Unsigned integer
uint64 unsigned = 2;
// Opaque value
string opaque = 3;
}
}
// Marker type for relocation arguments
message ArgumentRelocation {
}
message Relocation {
uint32 type = 1;
string type_name = 2;
RelocationTarget target = 3;
}
message RelocationTarget {
Symbol symbol = 1;
int64 addend = 2;
}
message InstructionDiff {
DiffKind diff_kind = 1;
optional Instruction instruction = 2;
optional InstructionBranchFrom branch_from = 3;
optional InstructionBranchTo branch_to = 4;
repeated ArgumentDiff arg_diff = 5;
}
message ArgumentDiff {
optional uint32 diff_index = 1;
}
enum DiffKind {
DIFF_NONE = 0;
DIFF_REPLACE = 1;
DIFF_DELETE = 2;
DIFF_INSERT = 3;
DIFF_OP_MISMATCH = 4;
DIFF_ARG_MISMATCH = 5;
}
message InstructionBranchFrom {
repeated uint32 instruction_index = 1;
uint32 branch_index = 2;
}
message InstructionBranchTo {
uint32 instruction_index = 1;
uint32 branch_index = 2;
}
message FunctionDiff {
Symbol symbol = 1;
repeated InstructionDiff instructions = 2;
optional float match_percent = 3;
}
message DataDiff {
DiffKind kind = 1;
bytes data = 2;
// May be larger than data
uint64 size = 3;
}
message SectionDiff {
string name = 1;
SectionKind kind = 2;
uint64 size = 3;
uint64 address = 4;
repeated FunctionDiff functions = 5;
repeated DataDiff data = 6;
optional float match_percent = 7;
}
enum SectionKind {
SECTION_UNKNOWN = 0;
SECTION_TEXT = 1;
SECTION_DATA = 2;
SECTION_BSS = 3;
SECTION_COMMON = 4;
}
message ObjectDiff {
repeated SectionDiff sections = 1;
}
message DiffResult {
optional ObjectDiff left = 1;
optional ObjectDiff right = 2;
}

Binary file not shown.

View File

@@ -0,0 +1,164 @@
syntax = "proto3";
package objdiff.report;
// Progress info for a report or unit
message Measures {
// Overall match percent, including partially matched functions and data
float fuzzy_match_percent = 1;
// Total size of code in bytes
uint64 total_code = 2;
// Fully matched code size in bytes
uint64 matched_code = 3;
// Fully matched code percent
float matched_code_percent = 4;
// Total size of data in bytes
uint64 total_data = 5;
// Fully matched data size in bytes
uint64 matched_data = 6;
// Fully matched data percent
float matched_data_percent = 7;
// Total number of functions
uint32 total_functions = 8;
// Fully matched functions
uint32 matched_functions = 9;
// Fully matched functions percent
float matched_functions_percent = 10;
// Completed (or "linked") code size in bytes
uint64 complete_code = 11;
// Completed (or "linked") code percent
float complete_code_percent = 12;
// Completed (or "linked") data size in bytes
uint64 complete_data = 13;
// Completed (or "linked") data percent
float complete_data_percent = 14;
// Total number of units
uint32 total_units = 15;
// Completed (or "linked") units
uint32 complete_units = 16;
}
// Project progress report
message Report {
// Overall progress info
Measures measures = 1;
// Units within this report
repeated ReportUnit units = 2;
// Report version
uint32 version = 3;
// Progress categories
repeated ReportCategory categories = 4;
}
message ReportCategory {
// The ID of the category
string id = 1;
// The name of the category
string name = 2;
// Progress info for this category
Measures measures = 3;
}
// A unit of the report (usually a translation unit)
message ReportUnit {
// The name of the unit
string name = 1;
// Progress info for this unit
Measures measures = 2;
// Sections within this unit
repeated ReportItem sections = 3;
// Functions within this unit
repeated ReportItem functions = 4;
// Extra metadata for this unit
optional ReportUnitMetadata metadata = 5;
}
// Extra metadata for a unit
message ReportUnitMetadata {
// Whether this unit is marked as complete (or "linked")
optional bool complete = 1;
// The name of the module this unit belongs to
optional string module_name = 2;
// The ID of the module this unit belongs to
optional uint32 module_id = 3;
// The path to the source file of this unit
optional string source_path = 4;
// Progress categories for this unit
repeated string progress_categories = 5;
// Whether this unit is automatically generated (not user-provided)
optional bool auto_generated = 6;
}
// A section or function within a unit
message ReportItem {
// The name of the item
string name = 1;
// The size of the item in bytes
uint64 size = 2;
// The overall match percent for this item
float fuzzy_match_percent = 3;
// Extra metadata for this item
optional ReportItemMetadata metadata = 4;
}
// Extra metadata for an item
message ReportItemMetadata {
// The demangled name of the function
optional string demangled_name = 1;
// The virtual address of the function or section
optional uint64 virtual_address = 2;
}
// A pair of reports to compare and generate changes
message ChangesInput {
// The previous report
Report from = 1;
// The current report
Report to = 2;
}
// Changes between two reports
message Changes {
// The progress info for the previous report
Measures from = 1;
// The progress info for the current report
Measures to = 2;
// Units that changed
repeated ChangeUnit units = 3;
}
// A changed unit
message ChangeUnit {
// The name of the unit
string name = 1;
// The previous progress info (omitted if new)
optional Measures from = 2;
// The current progress info (omitted if removed)
optional Measures to = 3;
// Sections that changed
repeated ChangeItem sections = 4;
// Functions that changed
repeated ChangeItem functions = 5;
// Extra metadata for this unit
optional ReportUnitMetadata metadata = 6;
}
// A changed section or function
message ChangeItem {
// The name of the item
string name = 1;
// The previous progress info (omitted if new)
optional ChangeItemInfo from = 2;
// The current progress info (omitted if removed)
optional ChangeItemInfo to = 3;
// Extra metadata for this item
optional ReportItemMetadata metadata = 4;
}
// Progress info for a section or function
message ChangeItemInfo {
// The overall match percent for this item
float fuzzy_match_percent = 1;
// The size of the item in bytes
uint64 size = 2;
}

View File

@@ -0,0 +1,441 @@
use std::{
borrow::Cow,
collections::{BTreeMap, HashMap},
};
use anyhow::{bail, Result};
use arm_attr::{enums::CpuArch, tag::Tag, BuildAttrs};
use object::{
elf::{self, SHT_ARM_ATTRIBUTES},
Endian, File, Object, ObjectSection, ObjectSymbol, Relocation, RelocationFlags, SectionIndex,
SectionKind, Symbol, SymbolKind,
};
use unarm::{
args::{Argument, OffsetImm, OffsetReg, Register},
parse::{ArmVersion, ParseMode, Parser},
DisplayOptions, ParseFlags, ParsedIns, RegNames,
};
use crate::{
arch::{ObjArch, ProcessCodeResult},
diff::{ArmArchVersion, ArmR9Usage, DiffObjConfig},
obj::{ObjIns, ObjInsArg, ObjInsArgValue, ObjReloc, ObjSection},
};
pub struct ObjArchArm {
/// Maps section index, to list of disasm modes (arm, thumb or data) sorted by address
disasm_modes: HashMap<SectionIndex, Vec<DisasmMode>>,
detected_version: Option<ArmVersion>,
endianness: object::Endianness,
}
impl ObjArchArm {
pub fn new(file: &File) -> Result<Self> {
let endianness = file.endianness();
match file {
File::Elf32(_) => {
let disasm_modes = Self::elf_get_mapping_symbols(file);
let detected_version = Self::elf_detect_arm_version(file)?;
Ok(Self { disasm_modes, detected_version, endianness })
}
_ => bail!("Unsupported file format {:?}", file.format()),
}
}
fn elf_detect_arm_version(file: &File) -> Result<Option<ArmVersion>> {
// Check ARM attributes
if let Some(arm_attrs) = file.sections().find(|s| {
s.kind() == SectionKind::Elf(SHT_ARM_ATTRIBUTES) && s.name() == Ok(".ARM.attributes")
}) {
let attr_data = arm_attrs.uncompressed_data()?;
let build_attrs = BuildAttrs::new(&attr_data, match file.endianness() {
object::Endianness::Little => arm_attr::Endian::Little,
object::Endianness::Big => arm_attr::Endian::Big,
})?;
for subsection in build_attrs.subsections() {
let subsection = subsection?;
if !subsection.is_aeabi() {
continue;
}
// Only checking first CpuArch tag. Others may exist, but that's very unlikely.
let cpu_arch = subsection.into_public_tag_iter()?.find_map(|(_, tag)| {
if let Tag::CpuArch(cpu_arch) = tag {
Some(cpu_arch)
} else {
None
}
});
match cpu_arch {
Some(CpuArch::V4T) => return Ok(Some(ArmVersion::V4T)),
Some(CpuArch::V5TE) => return Ok(Some(ArmVersion::V5Te)),
Some(CpuArch::V6K) => return Ok(Some(ArmVersion::V6K)),
Some(arch) => bail!("ARM arch {} not supported", arch),
None => {}
};
}
}
Ok(None)
}
fn elf_get_mapping_symbols(file: &File) -> HashMap<SectionIndex, Vec<DisasmMode>> {
file.sections()
.filter(|s| s.kind() == SectionKind::Text)
.map(|s| {
let index = s.index();
let mut mapping_symbols: Vec<_> = file
.symbols()
.filter(|s| s.section_index().map(|i| i == index).unwrap_or(false))
.filter_map(|s| DisasmMode::from_symbol(&s))
.collect();
mapping_symbols.sort_unstable_by_key(|x| x.address);
(s.index(), mapping_symbols)
})
.collect()
}
}
impl ObjArch for ObjArchArm {
fn symbol_address(&self, symbol: &Symbol) -> u64 {
let address = symbol.address();
if symbol.kind() == SymbolKind::Text {
address & !1
} else {
address
}
}
fn process_code(
&self,
address: u64,
code: &[u8],
section_index: usize,
relocations: &[ObjReloc],
line_info: &BTreeMap<u64, u32>,
config: &DiffObjConfig,
) -> Result<ProcessCodeResult> {
let start_addr = address as u32;
let end_addr = start_addr + code.len() as u32;
// Mapping symbols decide what kind of data comes after it. $a for ARM code, $t for Thumb code and $d for data.
let fallback_mappings = [DisasmMode { address: start_addr, mapping: ParseMode::Arm }];
let mapping_symbols = self
.disasm_modes
.get(&SectionIndex(section_index))
.map(|x| x.as_slice())
.unwrap_or(&fallback_mappings);
let first_mapping_idx = mapping_symbols
.binary_search_by_key(&start_addr, |x| x.address)
.unwrap_or_else(|idx| idx - 1);
let first_mapping = mapping_symbols[first_mapping_idx].mapping;
let mut mappings_iter =
mapping_symbols.iter().skip(first_mapping_idx + 1).take_while(|x| x.address < end_addr);
let mut next_mapping = mappings_iter.next();
let ins_count = code.len() / first_mapping.instruction_size(start_addr);
let mut ops = Vec::<u16>::with_capacity(ins_count);
let mut insts = Vec::<ObjIns>::with_capacity(ins_count);
let version = match config.arm_arch_version {
ArmArchVersion::Auto => self.detected_version.unwrap_or(ArmVersion::V5Te),
ArmArchVersion::V4T => ArmVersion::V4T,
ArmArchVersion::V5TE => ArmVersion::V5Te,
ArmArchVersion::V6K => ArmVersion::V6K,
};
let endian = match self.endianness {
object::Endianness::Little => unarm::Endian::Little,
object::Endianness::Big => unarm::Endian::Big,
};
let parse_flags = ParseFlags { ual: config.arm_unified_syntax, version };
let mut parser = Parser::new(first_mapping, start_addr, endian, parse_flags, code);
let display_options = DisplayOptions {
reg_names: RegNames {
av_registers: config.arm_av_registers,
r9_use: match config.arm_r9_usage {
ArmR9Usage::GeneralPurpose => unarm::R9Use::GeneralPurpose,
ArmR9Usage::Sb => unarm::R9Use::Pid,
ArmR9Usage::Tr => unarm::R9Use::Tls,
},
explicit_stack_limit: config.arm_sl_usage,
frame_pointer: config.arm_fp_usage,
ip: config.arm_ip_usage,
},
};
while let Some((address, ins, parsed_ins)) = parser.next() {
if let Some(next) = next_mapping {
let next_address = parser.address;
if next_address >= next.address {
// Change mapping
parser.mode = next.mapping;
next_mapping = mappings_iter.next();
}
}
let line = line_info.range(..=address as u64).last().map(|(_, &b)| b);
let reloc = relocations.iter().find(|r| (r.address as u32 & !1) == address).cloned();
let mut reloc_arg = None;
if let Some(reloc) = &reloc {
match reloc.flags {
// Calls
RelocationFlags::Elf { r_type: elf::R_ARM_THM_XPC22 }
| RelocationFlags::Elf { r_type: elf::R_ARM_THM_PC22 }
| RelocationFlags::Elf { r_type: elf::R_ARM_PC24 }
| RelocationFlags::Elf { r_type: elf::R_ARM_XPC25 }
| RelocationFlags::Elf { r_type: elf::R_ARM_CALL } => {
reloc_arg = parsed_ins
.args
.iter()
.rposition(|a| matches!(a, Argument::BranchDest(_)));
}
// Data
RelocationFlags::Elf { r_type: elf::R_ARM_ABS32 } => {
reloc_arg =
parsed_ins.args.iter().rposition(|a| matches!(a, Argument::UImm(_)));
}
_ => (),
}
};
let (args, branch_dest) = if reloc.is_some() && parser.mode == ParseMode::Data {
(vec![ObjInsArg::Reloc], None)
} else {
push_args(&parsed_ins, config, reloc_arg, address, display_options)?
};
ops.push(ins.opcode_id());
insts.push(ObjIns {
address: address as u64,
size: (parser.address - address) as u8,
op: ins.opcode_id(),
mnemonic: Cow::Borrowed(parsed_ins.mnemonic),
args,
reloc,
branch_dest,
line,
formatted: parsed_ins.display(display_options).to_string(),
orig: None,
});
}
Ok(ProcessCodeResult { ops, insts })
}
fn implcit_addend(
&self,
_file: &File<'_>,
section: &ObjSection,
address: u64,
reloc: &Relocation,
) -> Result<i64> {
let address = address as usize;
Ok(match reloc.flags() {
// ARM calls
RelocationFlags::Elf { r_type: elf::R_ARM_PC24 }
| RelocationFlags::Elf { r_type: elf::R_ARM_XPC25 }
| RelocationFlags::Elf { r_type: elf::R_ARM_CALL } => {
let data = section.data[address..address + 4].try_into()?;
let addend = self.endianness.read_i32_bytes(data);
let imm24 = addend & 0xffffff;
(imm24 << 2) << 8 >> 8
}
// Thumb calls
RelocationFlags::Elf { r_type: elf::R_ARM_THM_PC22 }
| RelocationFlags::Elf { r_type: elf::R_ARM_THM_XPC22 } => {
let data = section.data[address..address + 2].try_into()?;
let high = self.endianness.read_i16_bytes(data) as i32;
let data = section.data[address + 2..address + 4].try_into()?;
let low = self.endianness.read_i16_bytes(data) as i32;
let imm22 = ((high & 0x7ff) << 11) | (low & 0x7ff);
(imm22 << 1) << 9 >> 9
}
// Data
RelocationFlags::Elf { r_type: elf::R_ARM_ABS32 } => {
let data = section.data[address..address + 4].try_into()?;
self.endianness.read_i32_bytes(data)
}
flags => bail!("Unsupported ARM implicit relocation {flags:?}"),
} as i64)
}
fn demangle(&self, name: &str) -> Option<String> {
cpp_demangle::Symbol::new(name)
.ok()
.and_then(|s| s.demangle(&cpp_demangle::DemangleOptions::default()).ok())
}
fn display_reloc(&self, flags: RelocationFlags) -> Cow<'static, str> {
Cow::Owned(format!("<{flags:?}>"))
}
}
#[derive(Clone, Copy, Debug)]
struct DisasmMode {
address: u32,
mapping: ParseMode,
}
impl DisasmMode {
fn from_symbol<'a>(sym: &Symbol<'a, '_, &'a [u8]>) -> Option<Self> {
if let Ok(name) = sym.name() {
ParseMode::from_mapping_symbol(name)
.map(|mapping| DisasmMode { address: sym.address() as u32, mapping })
} else {
None
}
}
}
fn push_args(
parsed_ins: &ParsedIns,
config: &DiffObjConfig,
reloc_arg: Option<usize>,
cur_addr: u32,
display_options: DisplayOptions,
) -> Result<(Vec<ObjInsArg>, Option<u64>)> {
let mut args = vec![];
let mut branch_dest = None;
let mut writeback = false;
let mut deref = false;
for (i, arg) in parsed_ins.args_iter().enumerate() {
// Emit punctuation before separator
if deref {
match arg {
Argument::OffsetImm(OffsetImm { post_indexed: true, value: _ })
| Argument::OffsetReg(OffsetReg { add: _, post_indexed: true, reg: _ })
| Argument::CoOption(_) => {
deref = false;
args.push(ObjInsArg::PlainText("]".into()));
if writeback {
writeback = false;
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque("!".into())));
}
}
_ => {}
}
}
if i > 0 {
args.push(ObjInsArg::PlainText(config.separator().into()));
}
if reloc_arg == Some(i) {
args.push(ObjInsArg::Reloc);
} else {
match arg {
Argument::None => {}
Argument::Reg(reg) => {
if reg.deref {
deref = true;
args.push(ObjInsArg::PlainText("[".into()));
}
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(
reg.reg.display(display_options.reg_names).to_string().into(),
)));
if reg.writeback {
if reg.deref {
writeback = true;
} else {
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque("!".into())));
}
}
}
Argument::RegList(reg_list) => {
args.push(ObjInsArg::PlainText("{".into()));
let mut first = true;
for i in 0..16 {
if (reg_list.regs & (1 << i)) != 0 {
if !first {
args.push(ObjInsArg::PlainText(config.separator().into()));
}
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(
Register::parse(i)
.display(display_options.reg_names)
.to_string()
.into(),
)));
first = false;
}
}
args.push(ObjInsArg::PlainText("}".into()));
if reg_list.user_mode {
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque("^".to_string().into())));
}
}
Argument::UImm(value) | Argument::CoOpcode(value) | Argument::SatImm(value) => {
args.push(ObjInsArg::PlainText("#".into()));
args.push(ObjInsArg::Arg(ObjInsArgValue::Unsigned(*value as u64)));
}
Argument::SImm(value)
| Argument::OffsetImm(OffsetImm { post_indexed: _, value }) => {
args.push(ObjInsArg::PlainText("#".into()));
args.push(ObjInsArg::Arg(ObjInsArgValue::Signed(*value as i64)));
}
Argument::BranchDest(value) => {
let dest = cur_addr.wrapping_add_signed(*value) as u64;
args.push(ObjInsArg::BranchDest(dest));
branch_dest = Some(dest);
}
Argument::CoOption(value) => {
args.push(ObjInsArg::PlainText("{".into()));
args.push(ObjInsArg::Arg(ObjInsArgValue::Unsigned(*value as u64)));
args.push(ObjInsArg::PlainText("}".into()));
}
Argument::CoprocNum(value) => {
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(format!("p{}", value).into())));
}
Argument::ShiftImm(shift) => {
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(shift.op.to_string().into())));
args.push(ObjInsArg::PlainText(" #".into()));
args.push(ObjInsArg::Arg(ObjInsArgValue::Unsigned(shift.imm as u64)));
}
Argument::ShiftReg(shift) => {
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(shift.op.to_string().into())));
args.push(ObjInsArg::PlainText(" ".into()));
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(
shift.reg.display(display_options.reg_names).to_string().into(),
)));
}
Argument::OffsetReg(offset) => {
if !offset.add {
args.push(ObjInsArg::PlainText("-".into()));
}
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(
offset.reg.display(display_options.reg_names).to_string().into(),
)));
}
Argument::CpsrMode(mode) => {
args.push(ObjInsArg::PlainText("#".into()));
args.push(ObjInsArg::Arg(ObjInsArgValue::Unsigned(mode.mode as u64)));
if mode.writeback {
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque("!".into())));
}
}
Argument::CoReg(_)
| Argument::StatusReg(_)
| Argument::StatusMask(_)
| Argument::Shift(_)
| Argument::CpsrFlags(_)
| Argument::Endian(_) => args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(
arg.display(display_options, None).to_string().into(),
))),
}
}
}
if deref {
args.push(ObjInsArg::PlainText("]".into()));
if writeback {
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque("!".into())));
}
}
Ok((args, branch_dest))
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,316 @@
use std::{borrow::Cow, collections::BTreeMap, sync::Mutex};
use anyhow::{anyhow, bail, Result};
use object::{
elf, Endian, Endianness, File, FileFlags, Object, ObjectSection, ObjectSymbol, Relocation,
RelocationFlags, RelocationTarget,
};
use rabbitizer::{config, Abi, InstrCategory, Instruction, OperandType};
use crate::{
arch::{ObjArch, ProcessCodeResult},
diff::{DiffObjConfig, MipsAbi, MipsInstrCategory},
obj::{ObjIns, ObjInsArg, ObjInsArgValue, ObjReloc, ObjSection},
};
static RABBITIZER_MUTEX: Mutex<()> = Mutex::new(());
fn configure_rabbitizer(abi: Abi) {
unsafe {
config::RabbitizerConfig_Cfg.reg_names.fpr_abi_names = abi;
}
}
pub struct ObjArchMips {
pub endianness: Endianness,
pub abi: Abi,
pub instr_category: InstrCategory,
pub ri_gp_value: i32,
}
const EF_MIPS_ABI: u32 = 0x0000F000;
const EF_MIPS_MACH: u32 = 0x00FF0000;
const EF_MIPS_MACH_ALLEGREX: u32 = 0x00840000;
const EF_MIPS_MACH_5900: u32 = 0x00920000;
const R_MIPS15_S3: u32 = 119;
impl ObjArchMips {
pub fn new(object: &File) -> Result<Self> {
let mut abi = Abi::NUMERIC;
let mut instr_category = InstrCategory::CPU;
match object.flags() {
FileFlags::None => {}
FileFlags::Elf { e_flags, .. } => {
abi = match e_flags & EF_MIPS_ABI {
elf::EF_MIPS_ABI_O32 | elf::EF_MIPS_ABI_O64 => Abi::O32,
elf::EF_MIPS_ABI_EABI32 | elf::EF_MIPS_ABI_EABI64 => Abi::N32,
_ => {
if e_flags & elf::EF_MIPS_ABI2 != 0 {
Abi::N32
} else {
Abi::NUMERIC
}
}
};
instr_category = match e_flags & EF_MIPS_MACH {
EF_MIPS_MACH_ALLEGREX => InstrCategory::R4000ALLEGREX,
EF_MIPS_MACH_5900 => InstrCategory::R5900,
_ => InstrCategory::CPU,
};
}
_ => bail!("Unsupported MIPS file flags"),
}
// Parse the ri_gp_value stored in .reginfo to be able to correctly
// calculate R_MIPS_GPREL16 relocations later. The value is stored
// 0x14 bytes into .reginfo (on 32 bit platforms)
let ri_gp_value = object
.section_by_name(".reginfo")
.and_then(|section| section.data().ok())
.and_then(|data| data.get(0x14..0x18))
.and_then(|s| s.try_into().ok())
.map(|bytes| object.endianness().read_i32_bytes(bytes))
.unwrap_or(0);
Ok(Self { endianness: object.endianness(), abi, instr_category, ri_gp_value })
}
}
impl ObjArch for ObjArchMips {
fn process_code(
&self,
address: u64,
code: &[u8],
section_index: usize,
relocations: &[ObjReloc],
line_info: &BTreeMap<u64, u32>,
config: &DiffObjConfig,
) -> Result<ProcessCodeResult> {
let _guard = RABBITIZER_MUTEX.lock().map_err(|e| anyhow!("Failed to lock mutex: {e}"))?;
configure_rabbitizer(match config.mips_abi {
MipsAbi::Auto => self.abi,
MipsAbi::O32 => Abi::O32,
MipsAbi::N32 => Abi::N32,
MipsAbi::N64 => Abi::N64,
});
let instr_category = match config.mips_instr_category {
MipsInstrCategory::Auto => self.instr_category,
MipsInstrCategory::Cpu => InstrCategory::CPU,
MipsInstrCategory::Rsp => InstrCategory::RSP,
MipsInstrCategory::R3000Gte => InstrCategory::R3000GTE,
MipsInstrCategory::R4000Allegrex => InstrCategory::R4000ALLEGREX,
MipsInstrCategory::R5900 => InstrCategory::R5900,
};
let start_address = address;
let end_address = address + code.len() as u64;
let ins_count = code.len() / 4;
let mut ops = Vec::<u16>::with_capacity(ins_count);
let mut insts = Vec::<ObjIns>::with_capacity(ins_count);
let mut cur_addr = start_address as u32;
for chunk in code.chunks_exact(4) {
let reloc = relocations.iter().find(|r| (r.address as u32 & !3) == cur_addr);
let code = self.endianness.read_u32_bytes(chunk.try_into()?);
let instruction = Instruction::new(code, cur_addr, instr_category);
let formatted = instruction.disassemble(None, 0);
let op = instruction.unique_id as u16;
ops.push(op);
let mnemonic = instruction.opcode_name();
let is_branch = instruction.is_branch();
let branch_offset = instruction.branch_offset();
let mut branch_dest = if is_branch {
cur_addr.checked_add_signed(branch_offset).map(|a| a as u64)
} else {
None
};
let operands = instruction.get_operands_slice();
let mut args = Vec::with_capacity(operands.len() + 1);
for (idx, op) in operands.iter().enumerate() {
if idx > 0 {
args.push(ObjInsArg::PlainText(config.separator().into()));
}
match op {
OperandType::cpu_immediate
| OperandType::cpu_label
| OperandType::cpu_branch_target_label => {
if let Some(reloc) = reloc {
// If the relocation target is within the current function, we can
// convert it into a relative branch target. Note that we check
// 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)
{
let target_address = target_address.unwrap();
args.push(ObjInsArg::BranchDest(target_address));
branch_dest = Some(target_address);
} else {
push_reloc(&mut args, reloc)?;
branch_dest = None;
}
} else if let Some(branch_dest) = branch_dest {
args.push(ObjInsArg::BranchDest(branch_dest));
} else {
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(
op.disassemble(&instruction, None).into(),
)));
}
}
OperandType::cpu_immediate_base => {
if let Some(reloc) = reloc {
push_reloc(&mut args, reloc)?;
} else {
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(
OperandType::cpu_immediate.disassemble(&instruction, None).into(),
)));
}
args.push(ObjInsArg::PlainText("(".into()));
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(
OperandType::cpu_rs.disassemble(&instruction, None).into(),
)));
args.push(ObjInsArg::PlainText(")".into()));
}
// OperandType::r5900_immediate15 => match reloc {
// Some(reloc)
// if reloc.flags == RelocationFlags::Elf { r_type: R_MIPS15_S3 } =>
// {
// push_reloc(&mut args, reloc)?;
// }
// _ => {
// args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(
// op.disassemble(&instruction, None).into(),
// )));
// }
// },
_ => {
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(
op.disassemble(&instruction, None).into(),
)));
}
}
}
let line = line_info.range(..=cur_addr as u64).last().map(|(_, &b)| b);
insts.push(ObjIns {
address: cur_addr as u64,
size: 4,
op,
mnemonic: Cow::Borrowed(mnemonic),
args,
reloc: reloc.cloned(),
branch_dest,
line,
formatted,
orig: None,
});
cur_addr += 4;
}
Ok(ProcessCodeResult { ops, insts })
}
fn implcit_addend(
&self,
file: &File<'_>,
section: &ObjSection,
address: u64,
reloc: &Relocation,
) -> Result<i64> {
let data = section.data[address as usize..address as usize + 4].try_into()?;
let addend = self.endianness.read_u32_bytes(data);
Ok(match reloc.flags() {
RelocationFlags::Elf { r_type: elf::R_MIPS_32 } => addend as i64,
RelocationFlags::Elf { r_type: elf::R_MIPS_26 } => ((addend & 0x03FFFFFF) << 2) as i64,
RelocationFlags::Elf { r_type: elf::R_MIPS_HI16 } => {
((addend & 0x0000FFFF) << 16) as i32 as i64
}
RelocationFlags::Elf {
r_type: elf::R_MIPS_LO16 | elf::R_MIPS_GOT16 | elf::R_MIPS_CALL16,
} => (addend & 0x0000FFFF) as i16 as i64,
RelocationFlags::Elf { r_type: elf::R_MIPS_GPREL16 | elf::R_MIPS_LITERAL } => {
let RelocationTarget::Symbol(idx) = reloc.target() else {
bail!("Unsupported R_MIPS_GPREL16 relocation against a non-symbol");
};
let sym = file.symbol_by_index(idx)?;
// if the symbol we are relocating against is in a local section we need to add
// the ri_gp_value from .reginfo to the addend.
if sym.section().index().is_some() {
((addend & 0x0000FFFF) as i16 as i64) + self.ri_gp_value as i64
} else {
(addend & 0x0000FFFF) as i16 as i64
}
}
RelocationFlags::Elf { r_type: elf::R_MIPS_PC16 } => 0, // PC-relative relocation
RelocationFlags::Elf { r_type: R_MIPS15_S3 } => ((addend & 0x001FFFC0) >> 3) as i64,
flags => bail!("Unsupported MIPS implicit relocation {flags:?}"),
})
}
fn display_reloc(&self, flags: RelocationFlags) -> Cow<'static, str> {
match flags {
RelocationFlags::Elf { r_type } => match r_type {
elf::R_MIPS_32 => Cow::Borrowed("R_MIPS_32"),
elf::R_MIPS_26 => Cow::Borrowed("R_MIPS_26"),
elf::R_MIPS_HI16 => Cow::Borrowed("R_MIPS_HI16"),
elf::R_MIPS_LO16 => Cow::Borrowed("R_MIPS_LO16"),
elf::R_MIPS_GPREL16 => Cow::Borrowed("R_MIPS_GPREL16"),
elf::R_MIPS_LITERAL => Cow::Borrowed("R_MIPS_LITERAL"),
elf::R_MIPS_GOT16 => Cow::Borrowed("R_MIPS_GOT16"),
elf::R_MIPS_PC16 => Cow::Borrowed("R_MIPS_PC16"),
elf::R_MIPS_CALL16 => Cow::Borrowed("R_MIPS_CALL16"),
R_MIPS15_S3 => Cow::Borrowed("R_MIPS15_S3"),
_ => Cow::Owned(format!("<{flags:?}>")),
},
_ => Cow::Owned(format!("<{flags:?}>")),
}
}
}
fn push_reloc(args: &mut Vec<ObjInsArg>, reloc: &ObjReloc) -> Result<()> {
match reloc.flags {
RelocationFlags::Elf { r_type } => match r_type {
elf::R_MIPS_HI16 => {
args.push(ObjInsArg::PlainText("%hi(".into()));
args.push(ObjInsArg::Reloc);
args.push(ObjInsArg::PlainText(")".into()));
}
elf::R_MIPS_LO16 => {
args.push(ObjInsArg::PlainText("%lo(".into()));
args.push(ObjInsArg::Reloc);
args.push(ObjInsArg::PlainText(")".into()));
}
elf::R_MIPS_GOT16 => {
args.push(ObjInsArg::PlainText("%got(".into()));
args.push(ObjInsArg::Reloc);
args.push(ObjInsArg::PlainText(")".into()));
}
elf::R_MIPS_CALL16 => {
args.push(ObjInsArg::PlainText("%call16(".into()));
args.push(ObjInsArg::Reloc);
args.push(ObjInsArg::PlainText(")".into()));
}
elf::R_MIPS_GPREL16 => {
args.push(ObjInsArg::PlainText("%gp_rel(".into()));
args.push(ObjInsArg::Reloc);
args.push(ObjInsArg::PlainText(")".into()));
}
elf::R_MIPS_32
| elf::R_MIPS_26
| elf::R_MIPS_LITERAL
| elf::R_MIPS_PC16
| R_MIPS15_S3 => {
args.push(ObjInsArg::Reloc);
}
_ => bail!("Unsupported ELF MIPS relocation type {r_type}"),
},
flags => panic!("Unsupported MIPS relocation flags {flags:?}"),
}
Ok(())
}

View File

@@ -0,0 +1,174 @@
use std::{borrow::Cow, collections::BTreeMap, ffi::CStr};
use anyhow::{bail, Result};
use byteorder::ByteOrder;
use object::{Architecture, File, Object, ObjectSymbol, Relocation, RelocationFlags, Symbol};
use crate::{
diff::DiffObjConfig,
obj::{ObjIns, ObjReloc, ObjSection},
util::ReallySigned,
};
#[cfg(feature = "arm")]
mod arm;
#[cfg(feature = "arm64")]
mod arm64;
#[cfg(feature = "mips")]
pub mod mips;
#[cfg(feature = "ppc")]
pub mod ppc;
#[cfg(feature = "x86")]
pub mod x86;
/// Represents the type of data associated with an instruction
pub enum DataType {
Int8,
Int16,
Int32,
Int64,
Int128,
Float,
Double,
Bytes,
String,
}
impl DataType {
pub fn display_bytes<Endian: ByteOrder>(&self, bytes: &[u8]) -> Option<String> {
// 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;
}
match self {
DataType::Int8 => {
let i = i8::from_ne_bytes(bytes.try_into().unwrap());
if i < 0 {
format!("Int8: {:#x} ({:#x})", i, ReallySigned(i))
} else {
format!("Int8: {:#x}", i)
}
}
DataType::Int16 => {
let i = Endian::read_i16(bytes);
if i < 0 {
format!("Int16: {:#x} ({:#x})", i, ReallySigned(i))
} else {
format!("Int16: {:#x}", i)
}
}
DataType::Int32 => {
let i = Endian::read_i32(bytes);
if i < 0 {
format!("Int32: {:#x} ({:#x})", i, ReallySigned(i))
} else {
format!("Int32: {:#x}", i)
}
}
DataType::Int64 => {
let i = Endian::read_i64(bytes);
if i < 0 {
format!("Int64: {:#x} ({:#x})", i, ReallySigned(i))
} else {
format!("Int64: {:#x}", i)
}
}
DataType::Int128 => {
let i = Endian::read_i128(bytes);
if i < 0 {
format!("Int128: {:#x} ({:#x})", i, ReallySigned(i))
} else {
format!("Int128: {:#x}", i)
}
}
DataType::Float => {
format!("Float: {}", Endian::read_f32(bytes))
}
DataType::Double => {
format!("Double: {}", Endian::read_f64(bytes))
}
DataType::Bytes => {
format!("Bytes: {:#?}", bytes)
}
DataType::String => {
format!("String: {:?}", CStr::from_bytes_until_nul(bytes).ok()?)
}
}
.into()
}
fn required_len(&self) -> Option<usize> {
match self {
DataType::Int8 => Some(1),
DataType::Int16 => Some(2),
DataType::Int32 => Some(4),
DataType::Int64 => Some(8),
DataType::Int128 => Some(16),
DataType::Float => Some(4),
DataType::Double => Some(8),
DataType::Bytes => None,
DataType::String => None,
}
}
}
pub trait ObjArch: Send + Sync {
fn process_code(
&self,
address: u64,
code: &[u8],
section_index: usize,
relocations: &[ObjReloc],
line_info: &BTreeMap<u64, u32>,
config: &DiffObjConfig,
) -> Result<ProcessCodeResult>;
fn implcit_addend(
&self,
file: &File<'_>,
section: &ObjSection,
address: u64,
reloc: &Relocation,
) -> Result<i64>;
fn demangle(&self, _name: &str) -> Option<String> { None }
fn display_reloc(&self, flags: RelocationFlags) -> Cow<'static, str>;
fn symbol_address(&self, symbol: &Symbol) -> u64 { symbol.address() }
fn guess_data_type(&self, _instruction: &ObjIns) -> Option<DataType> { None }
fn display_data_type(&self, _ty: DataType, bytes: &[u8]) -> Option<String> {
Some(format!("Bytes: {:#x?}", bytes))
}
// Downcast methods
#[cfg(feature = "ppc")]
fn ppc(&self) -> Option<&ppc::ObjArchPpc> { None }
}
pub struct ProcessCodeResult {
pub ops: Vec<u16>,
pub insts: Vec<ObjIns>,
}
pub fn new_arch(object: &File) -> Result<Box<dyn ObjArch>> {
Ok(match object.architecture() {
#[cfg(feature = "ppc")]
Architecture::PowerPc => Box::new(ppc::ObjArchPpc::new(object)?),
#[cfg(feature = "mips")]
Architecture::Mips => Box::new(mips::ObjArchMips::new(object)?),
#[cfg(feature = "x86")]
Architecture::I386 | Architecture::X86_64 => Box::new(x86::ObjArchX86::new(object)?),
#[cfg(feature = "arm")]
Architecture::Arm => Box::new(arm::ObjArchArm::new(object)?),
#[cfg(feature = "arm64")]
Architecture::Aarch64 => Box::new(arm64::ObjArchArm64::new(object)?),
arch => bail!("Unsupported architecture: {arch:?}"),
})
}

View File

@@ -0,0 +1,383 @@
use std::{borrow::Cow, collections::BTreeMap};
use anyhow::{bail, ensure, Result};
use byteorder::BigEndian;
use cwextab::{decode_extab, ExceptionTableData};
use object::{
elf, File, Object, ObjectSection, ObjectSymbol, Relocation, RelocationFlags, RelocationTarget,
Symbol, SymbolKind,
};
use ppc750cl::{Argument, InsIter, Opcode, GPR};
use crate::{
arch::{DataType, ObjArch, ProcessCodeResult},
diff::DiffObjConfig,
obj::{ObjIns, ObjInsArg, ObjInsArgValue, ObjReloc, ObjSection, ObjSymbol},
};
// Relative relocation, can be Simm, Offset or BranchDest
fn is_relative_arg(arg: &Argument) -> bool {
matches!(arg, Argument::Simm(_) | Argument::Offset(_) | Argument::BranchDest(_))
}
// Relative or absolute relocation, can be Uimm, Simm or Offset
fn is_rel_abs_arg(arg: &Argument) -> bool {
matches!(arg, Argument::Uimm(_) | Argument::Simm(_) | Argument::Offset(_))
}
fn is_offset_arg(arg: &Argument) -> bool { matches!(arg, Argument::Offset(_)) }
pub struct ObjArchPpc {
/// Exception info
pub extab: Option<BTreeMap<usize, ExceptionInfo>>,
}
impl ObjArchPpc {
pub fn new(file: &File) -> Result<Self> { Ok(Self { extab: decode_exception_info(file)? }) }
}
impl ObjArch for ObjArchPpc {
fn process_code(
&self,
address: u64,
code: &[u8],
_section_index: usize,
relocations: &[ObjReloc],
line_info: &BTreeMap<u64, u32>,
config: &DiffObjConfig,
) -> Result<ProcessCodeResult> {
let ins_count = code.len() / 4;
let mut ops = Vec::<u16>::with_capacity(ins_count);
let mut insts = Vec::<ObjIns>::with_capacity(ins_count);
for (cur_addr, mut ins) in InsIter::new(code, address as u32) {
let reloc = relocations.iter().find(|r| (r.address as u32 & !3) == cur_addr);
if let Some(reloc) = reloc {
// Zero out relocations
ins.code = match reloc.flags {
RelocationFlags::Elf { r_type: elf::R_PPC_EMB_SDA21 } => ins.code & !0x1FFFFF,
RelocationFlags::Elf { r_type: elf::R_PPC_REL24 } => ins.code & !0x3FFFFFC,
RelocationFlags::Elf { r_type: elf::R_PPC_REL14 } => ins.code & !0xFFFC,
RelocationFlags::Elf {
r_type: elf::R_PPC_ADDR16_HI | elf::R_PPC_ADDR16_HA | elf::R_PPC_ADDR16_LO,
} => ins.code & !0xFFFF,
_ => ins.code,
};
}
let orig = ins.basic().to_string();
let simplified = ins.simplified();
let formatted = simplified.to_string();
let mut reloc_arg = None;
if let Some(reloc) = reloc {
match reloc.flags {
RelocationFlags::Elf { r_type: elf::R_PPC_EMB_SDA21 } => {
reloc_arg = Some(1);
}
RelocationFlags::Elf { r_type: elf::R_PPC_REL24 | elf::R_PPC_REL14 } => {
reloc_arg = simplified.args.iter().rposition(is_relative_arg);
}
RelocationFlags::Elf {
r_type: elf::R_PPC_ADDR16_HI | elf::R_PPC_ADDR16_HA | elf::R_PPC_ADDR16_LO,
} => {
reloc_arg = simplified.args.iter().rposition(is_rel_abs_arg);
}
_ => {}
}
}
let mut args = vec![];
let mut branch_dest = None;
let mut writing_offset = false;
for (idx, arg) in simplified.args_iter().enumerate() {
if idx > 0 && !writing_offset {
args.push(ObjInsArg::PlainText(config.separator().into()));
}
if reloc_arg == Some(idx) {
let reloc = reloc.unwrap();
push_reloc(&mut args, reloc)?;
// For @sda21, we can omit the register argument
if matches!(reloc.flags, RelocationFlags::Elf { r_type: elf::R_PPC_EMB_SDA21 })
// Sanity check: the next argument should be r0
&& matches!(simplified.args.get(idx + 1), Some(Argument::GPR(GPR(0))))
{
break;
}
} else {
match arg {
Argument::Simm(simm) => {
args.push(ObjInsArg::Arg(ObjInsArgValue::Signed(simm.0 as i64)));
}
Argument::Uimm(uimm) => {
args.push(ObjInsArg::Arg(ObjInsArgValue::Unsigned(uimm.0 as u64)));
}
Argument::Offset(offset) => {
args.push(ObjInsArg::Arg(ObjInsArgValue::Signed(offset.0 as i64)));
}
Argument::BranchDest(dest) => {
let dest = cur_addr.wrapping_add_signed(dest.0) as u64;
args.push(ObjInsArg::BranchDest(dest));
branch_dest = Some(dest);
}
_ => {
args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(
arg.to_string().into(),
)));
}
};
}
if writing_offset {
args.push(ObjInsArg::PlainText(")".into()));
writing_offset = false;
}
if is_offset_arg(arg) {
args.push(ObjInsArg::PlainText("(".into()));
writing_offset = true;
}
}
ops.push(ins.op as u16);
let line = line_info.range(..=cur_addr as u64).last().map(|(_, &b)| b);
insts.push(ObjIns {
address: cur_addr as u64,
size: 4,
mnemonic: Cow::Borrowed(simplified.mnemonic),
args,
reloc: reloc.cloned(),
op: ins.op as u16,
branch_dest,
line,
formatted,
orig: Some(orig),
});
}
Ok(ProcessCodeResult { ops, insts })
}
fn implcit_addend(
&self,
_file: &File<'_>,
_section: &ObjSection,
address: u64,
reloc: &Relocation,
) -> Result<i64> {
bail!("Unsupported PPC implicit relocation {:#x}:{:?}", address, reloc.flags())
}
fn demangle(&self, name: &str) -> Option<String> {
cwdemangle::demangle(name, &cwdemangle::DemangleOptions::default())
}
fn display_reloc(&self, flags: RelocationFlags) -> Cow<'static, str> {
match flags {
RelocationFlags::Elf { r_type } => match r_type {
elf::R_PPC_ADDR16_LO => Cow::Borrowed("R_PPC_ADDR16_LO"),
elf::R_PPC_ADDR16_HI => Cow::Borrowed("R_PPC_ADDR16_HI"),
elf::R_PPC_ADDR16_HA => Cow::Borrowed("R_PPC_ADDR16_HA"),
elf::R_PPC_EMB_SDA21 => Cow::Borrowed("R_PPC_EMB_SDA21"),
elf::R_PPC_ADDR32 => Cow::Borrowed("R_PPC_ADDR32"),
elf::R_PPC_UADDR32 => Cow::Borrowed("R_PPC_UADDR32"),
elf::R_PPC_REL24 => Cow::Borrowed("R_PPC_REL24"),
elf::R_PPC_REL14 => Cow::Borrowed("R_PPC_REL14"),
_ => Cow::Owned(format!("<{flags:?}>")),
},
_ => Cow::Owned(format!("<{flags:?}>")),
}
}
fn guess_data_type(&self, instruction: &ObjIns) -> Option<super::DataType> {
// Always shows the first string of the table. Not ideal, but it's really hard to find
// the actual string being referenced.
if instruction.reloc.as_ref().is_some_and(|r| r.target.name.starts_with("@stringBase")) {
return Some(DataType::String);
}
match Opcode::from(instruction.op as u8) {
Opcode::Lbz | Opcode::Lbzu | Opcode::Lbzux | Opcode::Lbzx => Some(DataType::Int8),
Opcode::Lhz | Opcode::Lhzu | Opcode::Lhzux | Opcode::Lhzx => Some(DataType::Int16),
Opcode::Lha | Opcode::Lhau | Opcode::Lhaux | Opcode::Lhax => Some(DataType::Int16),
Opcode::Lwz | Opcode::Lwzu | Opcode::Lwzux | Opcode::Lwzx => Some(DataType::Int32),
Opcode::Lfs | Opcode::Lfsu | Opcode::Lfsux | Opcode::Lfsx => Some(DataType::Float),
Opcode::Lfd | Opcode::Lfdu | Opcode::Lfdux | Opcode::Lfdx => Some(DataType::Double),
Opcode::Stb | Opcode::Stbu | Opcode::Stbux | Opcode::Stbx => Some(DataType::Int8),
Opcode::Sth | Opcode::Sthu | Opcode::Sthux | Opcode::Sthx => Some(DataType::Int16),
Opcode::Stw | Opcode::Stwu | Opcode::Stwux | Opcode::Stwx => Some(DataType::Int32),
Opcode::Stfs | Opcode::Stfsu | Opcode::Stfsux | Opcode::Stfsx => Some(DataType::Float),
Opcode::Stfd | Opcode::Stfdu | Opcode::Stfdux | Opcode::Stfdx => Some(DataType::Double),
_ => None,
}
}
fn display_data_type(&self, ty: DataType, bytes: &[u8]) -> Option<String> {
ty.display_bytes::<BigEndian>(bytes)
}
fn ppc(&self) -> Option<&ObjArchPpc> { Some(self) }
}
impl ObjArchPpc {
pub fn extab_for_symbol(&self, symbol: &ObjSymbol) -> Option<&ExceptionInfo> {
symbol.original_index.and_then(|i| self.extab.as_ref()?.get(&i))
}
}
fn push_reloc(args: &mut Vec<ObjInsArg>, reloc: &ObjReloc) -> Result<()> {
match reloc.flags {
RelocationFlags::Elf { r_type } => match r_type {
elf::R_PPC_ADDR16_LO => {
args.push(ObjInsArg::Reloc);
args.push(ObjInsArg::PlainText("@l".into()));
}
elf::R_PPC_ADDR16_HI => {
args.push(ObjInsArg::Reloc);
args.push(ObjInsArg::PlainText("@h".into()));
}
elf::R_PPC_ADDR16_HA => {
args.push(ObjInsArg::Reloc);
args.push(ObjInsArg::PlainText("@ha".into()));
}
elf::R_PPC_EMB_SDA21 => {
args.push(ObjInsArg::Reloc);
args.push(ObjInsArg::PlainText("@sda21".into()));
}
elf::R_PPC_ADDR32 | elf::R_PPC_UADDR32 | elf::R_PPC_REL24 | elf::R_PPC_REL14 => {
args.push(ObjInsArg::Reloc);
}
_ => bail!("Unsupported ELF PPC relocation type {r_type}"),
},
flags => bail!("Unsupported PPC relocation kind: {flags:?}"),
};
Ok(())
}
#[derive(Debug, Clone)]
pub struct ExtabSymbolRef {
pub original_index: usize,
pub name: String,
pub demangled_name: Option<String>,
}
#[derive(Debug, Clone)]
pub struct ExceptionInfo {
pub eti_symbol: ExtabSymbolRef,
pub etb_symbol: ExtabSymbolRef,
pub data: ExceptionTableData,
pub dtors: Vec<ExtabSymbolRef>,
}
fn decode_exception_info(file: &File<'_>) -> Result<Option<BTreeMap<usize, ExceptionInfo>>> {
let Some(extab_section) = file.section_by_name("extab") else {
return Ok(None);
};
let Some(extabindex_section) = file.section_by_name("extabindex") else {
return Ok(None);
};
let mut result = BTreeMap::new();
let extab_relocations = extab_section.relocations().collect::<BTreeMap<u64, Relocation>>();
let extabindex_relocations =
extabindex_section.relocations().collect::<BTreeMap<u64, Relocation>>();
for extabindex in file.symbols().filter(|symbol| {
symbol.section_index() == Some(extabindex_section.index())
&& symbol.kind() == SymbolKind::Data
}) {
if extabindex.size() != 12 {
log::warn!("Invalid extabindex entry size {}", extabindex.size());
continue;
}
// Each extabindex entry has two relocations:
// - 0x0: The function that the exception table is for
// - 0x8: The relevant entry in extab section
let Some(extab_func_reloc) = extabindex_relocations.get(&extabindex.address()) else {
log::warn!("Failed to find function relocation for extabindex entry");
continue;
};
let Some(extab_reloc) = extabindex_relocations.get(&(extabindex.address() + 8)) else {
log::warn!("Failed to find extab relocation for extabindex entry");
continue;
};
// Resolve the function and extab symbols
let Some(extab_func) = relocation_symbol(file, extab_func_reloc)? else {
log::warn!("Failed to find function symbol for extabindex entry");
continue;
};
let extab_func_name = extab_func.name()?;
let Some(extab) = relocation_symbol(file, extab_reloc)? else {
log::warn!("Failed to find extab symbol for extabindex entry");
continue;
};
let extab_start_addr = extab.address() - extab_section.address();
let extab_end_addr = extab_start_addr + extab.size();
// All relocations in the extab section are dtors
let mut dtors: Vec<ExtabSymbolRef> = vec![];
for (_, reloc) in extab_relocations.range(extab_start_addr..extab_end_addr) {
let Some(symbol) = relocation_symbol(file, reloc)? else {
log::warn!("Failed to find symbol for extab relocation");
continue;
};
dtors.push(make_symbol_ref(&symbol)?);
}
// Decode the extab data
let Some(extab_data) = extab_section.data_range(extab_start_addr, extab.size())? else {
log::warn!("Failed to get extab data for function {}", extab_func_name);
continue;
};
let data = match decode_extab(extab_data) {
Ok(decoded_data) => decoded_data,
Err(e) => {
log::warn!(
"Exception table decoding failed for function {}, reason: {}",
extab_func_name,
e.to_string()
);
return Ok(None);
}
};
//Add the new entry to the list
result.insert(extab_func.index().0, ExceptionInfo {
eti_symbol: make_symbol_ref(&extabindex)?,
etb_symbol: make_symbol_ref(&extab)?,
data,
dtors,
});
}
Ok(Some(result))
}
fn relocation_symbol<'data, 'file>(
file: &'file File<'data>,
relocation: &Relocation,
) -> Result<Option<Symbol<'data, 'file>>> {
let addend = relocation.addend();
match relocation.target() {
RelocationTarget::Symbol(idx) => {
ensure!(addend == 0, "Symbol relocations must have zero addend");
Ok(Some(file.symbol_by_index(idx)?))
}
RelocationTarget::Section(idx) => {
ensure!(addend >= 0, "Section relocations must have non-negative addend");
let addend = addend as u64;
Ok(file
.symbols()
.find(|symbol| symbol.section_index() == Some(idx) && symbol.address() == addend))
}
target => bail!("Unsupported relocation target: {target:?}"),
}
}
fn make_symbol_ref(symbol: &Symbol) -> Result<ExtabSymbolRef> {
let name = symbol.name()?.to_string();
let demangled_name = cwdemangle::demangle(&name, &cwdemangle::DemangleOptions::default());
Ok(ExtabSymbolRef { original_index: symbol.index().0, name, demangled_name })
}

View File

@@ -0,0 +1,344 @@
use std::{borrow::Cow, collections::BTreeMap};
use anyhow::{anyhow, bail, ensure, Result};
use iced_x86::{
Decoder, DecoderOptions, DecoratorKind, Formatter, FormatterOutput, FormatterTextKind,
GasFormatter, Instruction, IntelFormatter, MasmFormatter, NasmFormatter, NumberKind, OpKind,
PrefixKind, Register,
};
use object::{pe, Endian, Endianness, File, Object, Relocation, RelocationFlags};
use crate::{
arch::{ObjArch, ProcessCodeResult},
diff::{DiffObjConfig, X86Formatter},
obj::{ObjIns, ObjInsArg, ObjInsArgValue, ObjReloc, ObjSection},
};
pub struct ObjArchX86 {
bits: u32,
endianness: Endianness,
}
impl ObjArchX86 {
pub fn new(object: &File) -> Result<Self> {
Ok(Self { bits: if object.is_64() { 64 } else { 32 }, endianness: object.endianness() })
}
}
impl ObjArch for ObjArchX86 {
fn process_code(
&self,
address: u64,
code: &[u8],
_section_index: usize,
relocations: &[ObjReloc],
line_info: &BTreeMap<u64, u32>,
config: &DiffObjConfig,
) -> Result<ProcessCodeResult> {
let mut result = ProcessCodeResult { ops: Vec::new(), insts: Vec::new() };
let mut decoder = Decoder::with_ip(self.bits, code, address, DecoderOptions::NONE);
let mut formatter: Box<dyn Formatter> = match config.x86_formatter {
X86Formatter::Intel => Box::new(IntelFormatter::new()),
X86Formatter::Gas => Box::new(GasFormatter::new()),
X86Formatter::Nasm => Box::new(NasmFormatter::new()),
X86Formatter::Masm => Box::new(MasmFormatter::new()),
};
formatter.options_mut().set_space_after_operand_separator(config.space_between_args);
let mut output = InstructionFormatterOutput {
formatted: String::new(),
ins: ObjIns {
address: 0,
size: 0,
op: 0,
mnemonic: Cow::Borrowed("<invalid>"),
args: vec![],
reloc: None,
branch_dest: None,
line: None,
formatted: String::new(),
orig: None,
},
error: None,
ins_operands: vec![],
};
let mut instruction = Instruction::default();
while decoder.can_decode() {
decoder.decode_out(&mut instruction);
let address = instruction.ip();
let op = instruction.mnemonic() as u16;
let reloc = relocations
.iter()
.find(|r| r.address >= address && r.address < address + instruction.len() as u64);
let line = line_info.range(..=address).last().map(|(_, &b)| b);
output.ins = ObjIns {
address,
size: instruction.len() as u8,
op,
mnemonic: Cow::Borrowed("<invalid>"),
args: vec![],
reloc: reloc.cloned(),
branch_dest: None,
line,
formatted: String::new(),
orig: None,
};
// Run the formatter, which will populate output.ins
formatter.format(&instruction, &mut output);
if let Some(error) = output.error.take() {
return Err(error);
}
ensure!(output.ins_operands.len() == output.ins.args.len());
output.ins.formatted.clone_from(&output.formatted);
// Make sure we've put the relocation somewhere in the instruction
if reloc.is_some() && !output.ins.args.iter().any(|a| matches!(a, ObjInsArg::Reloc)) {
let mut found = replace_arg(
OpKind::Memory,
ObjInsArg::Reloc,
&mut output.ins.args,
&instruction,
&output.ins_operands,
)?;
if !found {
found = replace_arg(
OpKind::Immediate32,
ObjInsArg::Reloc,
&mut output.ins.args,
&instruction,
&output.ins_operands,
)?;
}
ensure!(found, "x86: Failed to find operand for Absolute relocation");
}
if reloc.is_some() && !output.ins.args.iter().any(|a| matches!(a, ObjInsArg::Reloc)) {
bail!("Failed to find relocation in instruction");
}
result.ops.push(op);
result.insts.push(output.ins.clone());
// Clear for next iteration
output.formatted.clear();
output.ins_operands.clear();
}
Ok(result)
}
fn implcit_addend(
&self,
_file: &File<'_>,
section: &ObjSection,
address: u64,
reloc: &Relocation,
) -> Result<i64> {
match reloc.flags() {
RelocationFlags::Coff { typ: pe::IMAGE_REL_I386_DIR32 | pe::IMAGE_REL_I386_REL32 } => {
let data = section.data[address as usize..address as usize + 4].try_into()?;
Ok(self.endianness.read_i32_bytes(data) as i64)
}
flags => bail!("Unsupported x86 implicit relocation {flags:?}"),
}
}
fn demangle(&self, name: &str) -> Option<String> {
if name.starts_with('?') {
msvc_demangler::demangle(name, msvc_demangler::DemangleFlags::llvm()).ok()
} else {
cpp_demangle::Symbol::new(name)
.ok()
.and_then(|s| s.demangle(&cpp_demangle::DemangleOptions::default()).ok())
}
}
fn display_reloc(&self, flags: RelocationFlags) -> Cow<'static, str> {
match flags {
RelocationFlags::Coff { typ } => match typ {
pe::IMAGE_REL_I386_DIR32 => Cow::Borrowed("IMAGE_REL_I386_DIR32"),
pe::IMAGE_REL_I386_REL32 => Cow::Borrowed("IMAGE_REL_I386_REL32"),
_ => Cow::Owned(format!("<{flags:?}>")),
},
_ => Cow::Owned(format!("<{flags:?}>")),
}
}
}
fn replace_arg(
from: OpKind,
to: ObjInsArg,
args: &mut [ObjInsArg],
instruction: &Instruction,
ins_operands: &[Option<u32>],
) -> Result<bool> {
let mut replace = None;
for i in 0..instruction.op_count() {
let op_kind = instruction.op_kind(i);
if op_kind == from {
replace = Some(i);
break;
}
}
if let Some(i) = replace {
for (j, arg) in args.iter_mut().enumerate() {
if ins_operands[j] == Some(i) {
*arg = to;
return Ok(true);
}
}
}
Ok(false)
}
struct InstructionFormatterOutput {
formatted: String,
ins: ObjIns,
error: Option<anyhow::Error>,
ins_operands: Vec<Option<u32>>,
}
impl InstructionFormatterOutput {
fn push_signed(&mut self, value: i64) {
// The formatter writes the '-' operator and then gives us a negative value,
// so convert it to a positive value to avoid double negatives
if value < 0
&& matches!(self.ins.args.last(), Some(ObjInsArg::Arg(ObjInsArgValue::Opaque(v))) if v == "-")
{
self.ins.args.push(ObjInsArg::Arg(ObjInsArgValue::Signed(value.wrapping_abs())));
} else {
self.ins.args.push(ObjInsArg::Arg(ObjInsArgValue::Signed(value)));
}
}
}
impl FormatterOutput for InstructionFormatterOutput {
fn write(&mut self, text: &str, kind: FormatterTextKind) {
self.formatted.push_str(text);
// Skip whitespace after the mnemonic
if self.ins.args.is_empty() && kind == FormatterTextKind::Text {
return;
}
self.ins_operands.push(None);
match kind {
FormatterTextKind::Text | FormatterTextKind::Punctuation => {
self.ins.args.push(ObjInsArg::PlainText(text.to_string().into()));
}
FormatterTextKind::Keyword | FormatterTextKind::Operator => {
self.ins.args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(text.to_string().into())));
}
_ => {
if self.error.is_none() {
self.error = Some(anyhow!("x86: Unsupported FormatterTextKind {:?}", kind));
}
}
}
}
fn write_prefix(&mut self, _instruction: &Instruction, text: &str, _prefix: PrefixKind) {
self.formatted.push_str(text);
self.ins_operands.push(None);
self.ins.args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(text.to_string().into())));
}
fn write_mnemonic(&mut self, _instruction: &Instruction, text: &str) {
self.formatted.push_str(text);
// TODO: can iced-x86 guarantee 'static here?
self.ins.mnemonic = Cow::Owned(text.to_string());
}
fn write_number(
&mut self,
_instruction: &Instruction,
_operand: u32,
instruction_operand: Option<u32>,
text: &str,
value: u64,
number_kind: NumberKind,
kind: FormatterTextKind,
) {
self.formatted.push_str(text);
self.ins_operands.push(instruction_operand);
// Handle relocations
match kind {
FormatterTextKind::LabelAddress => {
if let Some(reloc) = self.ins.reloc.as_ref() {
if matches!(reloc.flags, RelocationFlags::Coff {
typ: pe::IMAGE_REL_I386_DIR32 | pe::IMAGE_REL_I386_REL32
}) {
self.ins.args.push(ObjInsArg::Reloc);
return;
} else if self.error.is_none() {
self.error = Some(anyhow!(
"x86: Unsupported LabelAddress relocation flags {:?}",
reloc.flags
));
}
}
self.ins.args.push(ObjInsArg::BranchDest(value));
self.ins.branch_dest = Some(value);
return;
}
FormatterTextKind::FunctionAddress => {
if let Some(reloc) = self.ins.reloc.as_ref() {
if matches!(reloc.flags, RelocationFlags::Coff {
typ: pe::IMAGE_REL_I386_REL32
}) {
self.ins.args.push(ObjInsArg::Reloc);
return;
} else if self.error.is_none() {
self.error = Some(anyhow!(
"x86: Unsupported FunctionAddress relocation flags {:?}",
reloc.flags
));
}
}
}
_ => {}
}
match number_kind {
NumberKind::Int8 => {
self.push_signed(value as i8 as i64);
}
NumberKind::Int16 => {
self.push_signed(value as i16 as i64);
}
NumberKind::Int32 => {
self.push_signed(value as i32 as i64);
}
NumberKind::Int64 => {
self.push_signed(value as i64);
}
NumberKind::UInt8 | NumberKind::UInt16 | NumberKind::UInt32 | NumberKind::UInt64 => {
self.ins.args.push(ObjInsArg::Arg(ObjInsArgValue::Unsigned(value)));
}
}
}
fn write_decorator(
&mut self,
_instruction: &Instruction,
_operand: u32,
instruction_operand: Option<u32>,
text: &str,
_decorator: DecoratorKind,
) {
self.formatted.push_str(text);
self.ins_operands.push(instruction_operand);
self.ins.args.push(ObjInsArg::PlainText(text.to_string().into()));
}
fn write_register(
&mut self,
_instruction: &Instruction,
_operand: u32,
instruction_operand: Option<u32>,
text: &str,
_register: Register,
) {
self.formatted.push_str(text);
self.ins_operands.push(instruction_operand);
self.ins.args.push(ObjInsArg::Arg(ObjInsArgValue::Opaque(text.to_string().into())));
}
}

View File

@@ -0,0 +1,246 @@
#![allow(clippy::needless_lifetimes)] // Generated serde code
use crate::{
diff::{
ObjDataDiff, ObjDataDiffKind, ObjDiff, ObjInsArgDiff, ObjInsBranchFrom, ObjInsBranchTo,
ObjInsDiff, ObjInsDiffKind, ObjSectionDiff, ObjSymbolDiff,
},
obj::{
ObjInfo, ObjIns, ObjInsArg, ObjInsArgValue, ObjReloc, ObjSectionKind, ObjSymbol,
ObjSymbolFlagSet, ObjSymbolFlags,
},
};
// Protobuf diff types
include!(concat!(env!("OUT_DIR"), "/objdiff.diff.rs"));
include!(concat!(env!("OUT_DIR"), "/objdiff.diff.serde.rs"));
impl DiffResult {
pub fn new(left: Option<(&ObjInfo, &ObjDiff)>, right: Option<(&ObjInfo, &ObjDiff)>) -> Self {
Self {
left: left.map(|(obj, diff)| ObjectDiff::new(obj, diff)),
right: right.map(|(obj, diff)| ObjectDiff::new(obj, diff)),
}
}
}
impl ObjectDiff {
pub fn new(obj: &ObjInfo, diff: &ObjDiff) -> Self {
Self {
sections: diff
.sections
.iter()
.enumerate()
.map(|(i, d)| SectionDiff::new(obj, i, d))
.collect(),
}
}
}
impl SectionDiff {
pub fn new(obj: &ObjInfo, section_index: usize, section_diff: &ObjSectionDiff) -> Self {
let section = &obj.sections[section_index];
let functions = section_diff.symbols.iter().map(|d| FunctionDiff::new(obj, d)).collect();
let data = section_diff.data_diff.iter().map(|d| DataDiff::new(obj, d)).collect();
Self {
name: section.name.to_string(),
kind: SectionKind::from(section.kind) as i32,
size: section.size,
address: section.address,
functions,
data,
match_percent: section_diff.match_percent,
}
}
}
impl From<ObjSectionKind> for SectionKind {
fn from(value: ObjSectionKind) -> Self {
match value {
ObjSectionKind::Code => SectionKind::SectionText,
ObjSectionKind::Data => SectionKind::SectionData,
ObjSectionKind::Bss => SectionKind::SectionBss,
// TODO common
}
}
}
impl FunctionDiff {
pub fn new(object: &ObjInfo, symbol_diff: &ObjSymbolDiff) -> Self {
let (_section, symbol) = object.section_symbol(symbol_diff.symbol_ref);
// let diff_symbol = symbol_diff.diff_symbol.map(|symbol_ref| {
// let (_section, symbol) = object.section_symbol(symbol_ref);
// Symbol::from(symbol)
// });
let instructions = symbol_diff
.instructions
.iter()
.map(|ins_diff| InstructionDiff::new(object, ins_diff))
.collect();
Self {
symbol: Some(Symbol::new(symbol)),
// diff_symbol,
instructions,
match_percent: symbol_diff.match_percent,
}
}
}
impl DataDiff {
pub fn new(_object: &ObjInfo, data_diff: &ObjDataDiff) -> Self {
Self {
kind: DiffKind::from(data_diff.kind) as i32,
data: data_diff.data.clone(),
size: data_diff.len as u64,
}
}
}
impl Symbol {
pub fn new(value: &ObjSymbol) -> Self {
Self {
name: value.name.to_string(),
demangled_name: value.demangled_name.clone(),
address: value.address,
size: value.size,
flags: symbol_flags(value.flags),
}
}
}
fn symbol_flags(value: ObjSymbolFlagSet) -> u32 {
let mut flags = 0u32;
if value.0.contains(ObjSymbolFlags::Global) {
flags |= SymbolFlag::SymbolNone as u32;
}
if value.0.contains(ObjSymbolFlags::Local) {
flags |= SymbolFlag::SymbolLocal as u32;
}
if value.0.contains(ObjSymbolFlags::Weak) {
flags |= SymbolFlag::SymbolWeak as u32;
}
if value.0.contains(ObjSymbolFlags::Common) {
flags |= SymbolFlag::SymbolCommon as u32;
}
if value.0.contains(ObjSymbolFlags::Hidden) {
flags |= SymbolFlag::SymbolHidden as u32;
}
flags
}
impl Instruction {
pub fn new(object: &ObjInfo, instruction: &ObjIns) -> Self {
Self {
address: instruction.address,
size: instruction.size as u32,
opcode: instruction.op as u32,
mnemonic: instruction.mnemonic.to_string(),
formatted: instruction.formatted.clone(),
arguments: instruction.args.iter().map(Argument::new).collect(),
relocation: instruction.reloc.as_ref().map(|reloc| Relocation::new(object, reloc)),
branch_dest: instruction.branch_dest,
line_number: instruction.line,
original: instruction.orig.clone(),
}
}
}
impl Argument {
pub fn new(value: &ObjInsArg) -> Self {
Self {
value: Some(match value {
ObjInsArg::PlainText(s) => argument::Value::PlainText(s.to_string()),
ObjInsArg::Arg(v) => argument::Value::Argument(ArgumentValue::new(v)),
ObjInsArg::Reloc => argument::Value::Relocation(ArgumentRelocation {}),
ObjInsArg::BranchDest(dest) => argument::Value::BranchDest(*dest),
}),
}
}
}
impl ArgumentValue {
pub fn new(value: &ObjInsArgValue) -> Self {
Self {
value: Some(match value {
ObjInsArgValue::Signed(v) => argument_value::Value::Signed(*v),
ObjInsArgValue::Unsigned(v) => argument_value::Value::Unsigned(*v),
ObjInsArgValue::Opaque(v) => argument_value::Value::Opaque(v.to_string()),
}),
}
}
}
impl Relocation {
pub fn new(object: &ObjInfo, reloc: &ObjReloc) -> Self {
Self {
r#type: match reloc.flags {
object::RelocationFlags::Elf { r_type } => r_type,
object::RelocationFlags::MachO { r_type, .. } => r_type as u32,
object::RelocationFlags::Coff { typ } => typ as u32,
object::RelocationFlags::Xcoff { r_rtype, .. } => r_rtype as u32,
_ => unreachable!(),
},
type_name: object.arch.display_reloc(reloc.flags).into_owned(),
target: Some(RelocationTarget {
symbol: Some(Symbol::new(&reloc.target)),
addend: reloc.addend,
}),
}
}
}
impl InstructionDiff {
pub fn new(object: &ObjInfo, instruction_diff: &ObjInsDiff) -> Self {
Self {
instruction: instruction_diff.ins.as_ref().map(|ins| Instruction::new(object, ins)),
diff_kind: DiffKind::from(instruction_diff.kind) as i32,
branch_from: instruction_diff.branch_from.as_ref().map(InstructionBranchFrom::new),
branch_to: instruction_diff.branch_to.as_ref().map(InstructionBranchTo::new),
arg_diff: instruction_diff.arg_diff.iter().map(ArgumentDiff::new).collect(),
}
}
}
impl ArgumentDiff {
pub fn new(value: &Option<ObjInsArgDiff>) -> Self {
Self { diff_index: value.as_ref().map(|v| v.idx as u32) }
}
}
impl From<ObjInsDiffKind> for DiffKind {
fn from(value: ObjInsDiffKind) -> Self {
match value {
ObjInsDiffKind::None => DiffKind::DiffNone,
ObjInsDiffKind::OpMismatch => DiffKind::DiffOpMismatch,
ObjInsDiffKind::ArgMismatch => DiffKind::DiffArgMismatch,
ObjInsDiffKind::Replace => DiffKind::DiffReplace,
ObjInsDiffKind::Delete => DiffKind::DiffDelete,
ObjInsDiffKind::Insert => DiffKind::DiffInsert,
}
}
}
impl From<ObjDataDiffKind> for DiffKind {
fn from(value: ObjDataDiffKind) -> Self {
match value {
ObjDataDiffKind::None => DiffKind::DiffNone,
ObjDataDiffKind::Replace => DiffKind::DiffReplace,
ObjDataDiffKind::Delete => DiffKind::DiffDelete,
ObjDataDiffKind::Insert => DiffKind::DiffInsert,
}
}
}
impl InstructionBranchFrom {
pub fn new(value: &ObjInsBranchFrom) -> Self {
Self {
instruction_index: value.ins_idx.iter().map(|&x| x as u32).collect(),
branch_index: value.branch_idx as u32,
}
}
}
impl InstructionBranchTo {
pub fn new(value: &ObjInsBranchTo) -> Self {
Self { instruction_index: value.ins_idx as u32, branch_index: value.branch_idx as u32 }
}
}

View File

@@ -0,0 +1,5 @@
#[cfg(feature = "any-arch")]
pub mod diff;
pub mod report;
#[cfg(feature = "wasm")]
pub mod wasm;

View File

@@ -0,0 +1,442 @@
#![allow(clippy::needless_lifetimes)] // Generated serde code
use std::ops::AddAssign;
use anyhow::{bail, Result};
use prost::Message;
use serde_json::error::Category;
// Protobuf report types
include!(concat!(env!("OUT_DIR"), "/objdiff.report.rs"));
include!(concat!(env!("OUT_DIR"), "/objdiff.report.serde.rs"));
pub const REPORT_VERSION: u32 = 2;
impl Report {
/// Attempts to parse the report as binary protobuf or JSON.
pub fn parse(data: &[u8]) -> Result<Self> {
if data.is_empty() {
bail!(std::io::Error::from(std::io::ErrorKind::UnexpectedEof));
}
let report = if data[0] == b'{' {
// Load as JSON
Self::from_json(data)?
} else {
// Load as binary protobuf
Self::decode(data)?
};
Ok(report)
}
/// Attempts to parse the report as JSON, migrating from the legacy report format if necessary.
fn from_json(bytes: &[u8]) -> Result<Self, serde_json::Error> {
match serde_json::from_slice::<Self>(bytes) {
Ok(report) => Ok(report),
Err(e) => {
match e.classify() {
Category::Io | Category::Eof | Category::Syntax => Err(e),
Category::Data => {
// Try to load as legacy report
match serde_json::from_slice::<LegacyReport>(bytes) {
Ok(legacy_report) => Ok(Report::from(legacy_report)),
Err(_) => Err(e),
}
}
}
}
}
}
/// Migrates the report to the latest version.
/// Fails if the report version is newer than supported.
pub fn migrate(&mut self) -> Result<()> {
if self.version == 0 {
self.migrate_v0()?;
}
if self.version == 1 {
self.migrate_v1()?;
}
if self.version != REPORT_VERSION {
bail!("Unsupported report version: {}", self.version);
}
Ok(())
}
/// Adds `complete_code`, `complete_data`, `complete_code_percent`, and `complete_data_percent`
/// to measures, and sets `progress_categories` in unit metadata.
fn migrate_v0(&mut self) -> Result<()> {
let Some(measures) = &mut self.measures else {
bail!("Missing measures in report");
};
for unit in &mut self.units {
let Some(unit_measures) = &mut unit.measures else {
bail!("Missing measures in report unit");
};
let mut complete = false;
if let Some(metadata) = &mut unit.metadata {
if metadata.module_name.is_some() || metadata.module_id.is_some() {
metadata.progress_categories = vec!["modules".to_string()];
} else {
metadata.progress_categories = vec!["dol".to_string()];
}
complete = metadata.complete.unwrap_or(false);
};
if complete {
unit_measures.complete_code = unit_measures.total_code;
unit_measures.complete_data = unit_measures.total_data;
unit_measures.complete_code_percent = 100.0;
unit_measures.complete_data_percent = 100.0;
} else {
unit_measures.complete_code = 0;
unit_measures.complete_data = 0;
unit_measures.complete_code_percent = 0.0;
unit_measures.complete_data_percent = 0.0;
}
measures.complete_code += unit_measures.complete_code;
measures.complete_data += unit_measures.complete_data;
}
measures.calc_matched_percent();
self.calculate_progress_categories();
self.version = 1;
Ok(())
}
/// Adds `total_units` and `complete_units` to measures.
fn migrate_v1(&mut self) -> Result<()> {
let Some(total_measures) = &mut self.measures else {
bail!("Missing measures in report");
};
for unit in &mut self.units {
let Some(measures) = &mut unit.measures else {
bail!("Missing measures in report unit");
};
let complete = unit.metadata.as_ref().and_then(|m| m.complete).unwrap_or(false) as u32;
let progress_categories =
unit.metadata.as_ref().map(|m| m.progress_categories.as_slice()).unwrap_or(&[]);
measures.total_units = 1;
measures.complete_units = complete;
total_measures.total_units += 1;
total_measures.complete_units += complete;
for id in progress_categories {
if let Some(category) = self.categories.iter_mut().find(|c| &c.id == id) {
let Some(measures) = &mut category.measures else {
bail!("Missing measures in category");
};
measures.total_units += 1;
measures.complete_units += complete;
}
}
}
self.version = 2;
Ok(())
}
/// Calculate progress categories based on unit metadata.
pub fn calculate_progress_categories(&mut self) {
for unit in &self.units {
let Some(metadata) = unit.metadata.as_ref() else {
continue;
};
let Some(measures) = unit.measures.as_ref() else {
continue;
};
for category_id in &metadata.progress_categories {
let category = match self.categories.iter_mut().find(|c| &c.id == category_id) {
Some(category) => category,
None => {
self.categories.push(ReportCategory {
id: category_id.clone(),
name: String::new(),
measures: Some(Default::default()),
});
self.categories.last_mut().unwrap()
}
};
*category.measures.get_or_insert_with(Default::default) += *measures;
}
}
for category in &mut self.categories {
let measures = category.measures.get_or_insert_with(Default::default);
measures.calc_fuzzy_match_percent();
measures.calc_matched_percent();
}
}
/// Split the report into multiple reports based on progress categories.
/// Assumes progress categories are in the format `version`, `version.category`.
/// This is a hack for projects that generate all versions in a single report.
pub fn split(self) -> Vec<(String, Report)> {
let mut reports = Vec::new();
// Map units to Option to allow taking ownership
let mut units = self.units.into_iter().map(Some).collect::<Vec<_>>();
for category in &self.categories {
if category.id.contains(".") {
// Skip subcategories
continue;
}
fn is_sub_category(id: &str, parent: &str, sep: char) -> bool {
id.starts_with(parent) && id.get(parent.len()..).is_some_and(|s| s.starts_with(sep))
}
let mut sub_categories = self
.categories
.iter()
.filter(|c| is_sub_category(&c.id, &category.id, '.'))
.cloned()
.collect::<Vec<_>>();
// Remove category prefix
for sub_category in &mut sub_categories {
sub_category.id = sub_category.id[category.id.len() + 1..].to_string();
}
let mut sub_units = units
.iter_mut()
.filter_map(|opt| {
let unit = opt.as_mut()?;
let metadata = unit.metadata.as_ref()?;
if metadata.progress_categories.contains(&category.id) {
opt.take()
} else {
None
}
})
.collect::<Vec<_>>();
for sub_unit in &mut sub_units {
// Remove leading version/ from unit name
if let Some(name) =
sub_unit.name.strip_prefix(&category.id).and_then(|s| s.strip_prefix('/'))
{
sub_unit.name = name.to_string();
}
// Filter progress categories
let Some(metadata) = sub_unit.metadata.as_mut() else {
continue;
};
metadata.progress_categories = metadata
.progress_categories
.iter()
.filter(|c| is_sub_category(c, &category.id, '.'))
.map(|c| c[category.id.len() + 1..].to_string())
.collect();
}
reports.push((category.id.clone(), Report {
measures: category.measures,
units: sub_units,
version: self.version,
categories: sub_categories,
}));
}
reports
}
}
impl Measures {
/// Average the fuzzy match percentage over total code bytes.
pub fn calc_fuzzy_match_percent(&mut self) {
if self.total_code == 0 {
self.fuzzy_match_percent = 100.0;
} else {
self.fuzzy_match_percent /= self.total_code as f32;
}
}
/// Calculate the percentage of matched code, data, and functions.
pub fn calc_matched_percent(&mut self) {
self.matched_code_percent = if self.total_code == 0 {
100.0
} else {
self.matched_code as f32 / self.total_code as f32 * 100.0
};
self.matched_data_percent = if self.total_data == 0 {
100.0
} else {
self.matched_data as f32 / self.total_data as f32 * 100.0
};
self.matched_functions_percent = if self.total_functions == 0 {
100.0
} else {
self.matched_functions as f32 / self.total_functions as f32 * 100.0
};
self.complete_code_percent = if self.total_code == 0 {
100.0
} else {
self.complete_code as f32 / self.total_code as f32 * 100.0
};
self.complete_data_percent = if self.total_data == 0 {
100.0
} else {
self.complete_data as f32 / self.total_data as f32 * 100.0
};
}
}
impl From<&ReportItem> for ChangeItemInfo {
fn from(value: &ReportItem) -> Self {
Self { fuzzy_match_percent: value.fuzzy_match_percent, size: value.size }
}
}
impl AddAssign for Measures {
fn add_assign(&mut self, other: Self) {
self.fuzzy_match_percent += other.fuzzy_match_percent * other.total_code as f32;
self.total_code += other.total_code;
self.matched_code += other.matched_code;
self.total_data += other.total_data;
self.matched_data += other.matched_data;
self.total_functions += other.total_functions;
self.matched_functions += other.matched_functions;
self.complete_code += other.complete_code;
self.complete_data += other.complete_data;
self.total_units += other.total_units;
self.complete_units += other.complete_units;
}
}
/// Allows [collect](Iterator::collect) to be used on an iterator of [Measures].
impl FromIterator<Measures> for Measures {
fn from_iter<T>(iter: T) -> Self
where T: IntoIterator<Item = Measures> {
let mut measures = Measures::default();
for other in iter {
measures += other;
}
measures.calc_fuzzy_match_percent();
measures.calc_matched_percent();
measures
}
}
// Older JSON report types
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
struct LegacyReport {
fuzzy_match_percent: f32,
total_code: u64,
matched_code: u64,
matched_code_percent: f32,
total_data: u64,
matched_data: u64,
matched_data_percent: f32,
total_functions: u32,
matched_functions: u32,
matched_functions_percent: f32,
units: Vec<LegacyReportUnit>,
}
impl From<LegacyReport> for Report {
fn from(value: LegacyReport) -> Self {
Self {
measures: Some(Measures {
fuzzy_match_percent: value.fuzzy_match_percent,
total_code: value.total_code,
matched_code: value.matched_code,
matched_code_percent: value.matched_code_percent,
total_data: value.total_data,
matched_data: value.matched_data,
matched_data_percent: value.matched_data_percent,
total_functions: value.total_functions,
matched_functions: value.matched_functions,
matched_functions_percent: value.matched_functions_percent,
..Default::default()
}),
units: value.units.into_iter().map(ReportUnit::from).collect::<Vec<_>>(),
..Default::default()
}
}
}
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
struct LegacyReportUnit {
name: String,
fuzzy_match_percent: f32,
total_code: u64,
matched_code: u64,
total_data: u64,
matched_data: u64,
total_functions: u32,
matched_functions: u32,
#[serde(skip_serializing_if = "Option::is_none")]
complete: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
module_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
module_id: Option<u32>,
sections: Vec<LegacyReportItem>,
functions: Vec<LegacyReportItem>,
}
impl From<LegacyReportUnit> for ReportUnit {
fn from(value: LegacyReportUnit) -> Self {
let mut measures = Measures {
fuzzy_match_percent: value.fuzzy_match_percent,
total_code: value.total_code,
matched_code: value.matched_code,
total_data: value.total_data,
matched_data: value.matched_data,
total_functions: value.total_functions,
matched_functions: value.matched_functions,
..Default::default()
};
measures.calc_matched_percent();
Self {
name: value.name.clone(),
measures: Some(measures),
sections: value.sections.into_iter().map(ReportItem::from).collect(),
functions: value.functions.into_iter().map(ReportItem::from).collect(),
metadata: Some(ReportUnitMetadata {
complete: value.complete,
module_name: value.module_name.clone(),
module_id: value.module_id,
..Default::default()
}),
}
}
}
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
struct LegacyReportItem {
name: String,
#[serde(skip_serializing_if = "Option::is_none")]
demangled_name: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
serialize_with = "serialize_hex",
deserialize_with = "deserialize_hex"
)]
address: Option<u64>,
size: u64,
fuzzy_match_percent: f32,
}
impl From<LegacyReportItem> for ReportItem {
fn from(value: LegacyReportItem) -> Self {
Self {
name: value.name,
size: value.size,
fuzzy_match_percent: value.fuzzy_match_percent,
metadata: Some(ReportItemMetadata {
demangled_name: value.demangled_name,
virtual_address: value.address,
}),
}
}
}
fn serialize_hex<S>(x: &Option<u64>, s: S) -> Result<S::Ok, S::Error>
where S: serde::Serializer {
if let Some(x) = x {
s.serialize_str(&format!("{:#x}", x))
} else {
s.serialize_none()
}
}
fn deserialize_hex<'de, D>(d: D) -> Result<Option<u64>, D::Error>
where D: serde::Deserializer<'de> {
use serde::Deserialize;
let s = String::deserialize(d)?;
if s.is_empty() {
Ok(None)
} else if !s.starts_with("0x") {
Err(serde::de::Error::custom("expected hex string"))
} else {
u64::from_str_radix(&s[2..], 16).map(Some).map_err(serde::de::Error::custom)
}
}

View File

@@ -0,0 +1,78 @@
use prost::Message;
use wasm_bindgen::prelude::*;
use crate::{bindings::diff::DiffResult, diff, obj};
fn parse_object(
data: Option<Box<[u8]>>,
config: &diff::DiffObjConfig,
) -> Result<Option<obj::ObjInfo>, JsError> {
data.as_ref().map(|data| obj::read::parse(data, config)).transpose().to_js()
}
fn parse_and_run_diff(
left: Option<Box<[u8]>>,
right: Option<Box<[u8]>>,
config: diff::DiffObjConfig,
) -> Result<DiffResult, JsError> {
let target = parse_object(left, &config)?;
let base = parse_object(right, &config)?;
run_diff(target.as_ref(), base.as_ref(), config)
}
fn run_diff(
left: Option<&obj::ObjInfo>,
right: Option<&obj::ObjInfo>,
config: diff::DiffObjConfig,
) -> Result<DiffResult, JsError> {
log::debug!("Running diff with config: {:?}", config);
let result = diff::diff_objs(&config, left, right, None).to_js()?;
let left = left.and_then(|o| result.left.as_ref().map(|d| (o, d)));
let right = right.and_then(|o| result.right.as_ref().map(|d| (o, d)));
Ok(DiffResult::new(left, right))
}
// #[wasm_bindgen]
// pub fn run_diff_json(
// left: Option<Box<[u8]>>,
// right: Option<Box<[u8]>>,
// config: diff::DiffObjConfig,
// ) -> Result<String, JsError> {
// let out = run_diff_opt_box(left, right, config)?;
// serde_json::to_string(&out).map_err(|e| JsError::new(&e.to_string()))
// }
#[wasm_bindgen]
pub fn run_diff_proto(
left: Option<Box<[u8]>>,
right: Option<Box<[u8]>>,
config: diff::DiffObjConfig,
) -> Result<Box<[u8]>, JsError> {
let out = parse_and_run_diff(left, right, config)?;
Ok(out.encode_to_vec().into_boxed_slice())
}
#[wasm_bindgen(start)]
fn start() -> Result<(), JsError> {
console_error_panic_hook::set_once();
#[cfg(debug_assertions)]
console_log::init_with_level(log::Level::Debug).to_js()?;
#[cfg(not(debug_assertions))]
console_log::init_with_level(log::Level::Info).to_js()?;
Ok(())
}
#[inline]
fn to_js_error(e: impl std::fmt::Display) -> JsError { JsError::new(&e.to_string()) }
trait ToJsResult {
type Output;
fn to_js(self) -> Result<Self::Output, JsError>;
}
impl<T, E: std::fmt::Display> ToJsResult for Result<T, E> {
type Output = T;
fn to_js(self) -> Result<T, JsError> { self.map_err(to_js_error) }
}

View File

@@ -0,0 +1,269 @@
use std::{
fs,
fs::File,
io::{BufReader, BufWriter, Read},
path::{Path, PathBuf},
};
use anyhow::{anyhow, Context, Result};
use bimap::BiBTreeMap;
use filetime::FileTime;
use globset::{Glob, GlobSet, GlobSetBuilder};
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
pub struct ProjectConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub min_version: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub custom_make: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub custom_args: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub target_dir: Option<PathBuf>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub base_dir: Option<PathBuf>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub build_base: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub build_target: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub watch_patterns: Option<Vec<Glob>>,
#[serde(default, alias = "objects", skip_serializing_if = "Option::is_none")]
pub units: Option<Vec<ProjectObject>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub progress_categories: Option<Vec<ProjectProgressCategory>>,
}
impl ProjectConfig {
#[inline]
pub fn units(&self) -> &[ProjectObject] { self.units.as_deref().unwrap_or_default() }
#[inline]
pub fn units_mut(&mut self) -> &mut Vec<ProjectObject> {
self.units.get_or_insert_with(Vec::new)
}
#[inline]
pub fn progress_categories(&self) -> &[ProjectProgressCategory] {
self.progress_categories.as_deref().unwrap_or_default()
}
#[inline]
pub fn progress_categories_mut(&mut self) -> &mut Vec<ProjectProgressCategory> {
self.progress_categories.get_or_insert_with(Vec::new)
}
}
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
pub struct ProjectObject {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub path: Option<PathBuf>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub target_path: Option<PathBuf>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub base_path: Option<PathBuf>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[deprecated(note = "Use metadata.reverse_fn_order")]
pub reverse_fn_order: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[deprecated(note = "Use metadata.complete")]
pub complete: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub scratch: Option<ScratchConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<ProjectObjectMetadata>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub symbol_mappings: Option<SymbolMappings>,
}
pub type SymbolMappings = BiBTreeMap<String, String>;
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
pub struct ProjectObjectMetadata {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub complete: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reverse_fn_order: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source_path: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub progress_categories: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub auto_generated: Option<bool>,
}
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
pub struct ProjectProgressCategory {
#[serde(default)]
pub id: String,
#[serde(default)]
pub name: String,
}
impl ProjectObject {
pub fn name(&self) -> &str {
if let Some(name) = &self.name {
name
} else if let Some(path) = &self.path {
path.to_str().unwrap_or("[invalid path]")
} else {
"[unknown]"
}
}
pub fn resolve_paths(
&mut self,
project_dir: &Path,
target_obj_dir: Option<&Path>,
base_obj_dir: Option<&Path>,
) {
if let (Some(target_obj_dir), Some(path), None) =
(target_obj_dir, &self.path, &self.target_path)
{
self.target_path = Some(target_obj_dir.join(path));
} else if let Some(path) = &self.target_path {
self.target_path = Some(project_dir.join(path));
}
if let (Some(base_obj_dir), Some(path), None) = (base_obj_dir, &self.path, &self.base_path)
{
self.base_path = Some(base_obj_dir.join(path));
} else if let Some(path) = &self.base_path {
self.base_path = Some(project_dir.join(path));
}
}
pub fn complete(&self) -> Option<bool> {
#[expect(deprecated)]
self.metadata.as_ref().and_then(|m| m.complete).or(self.complete)
}
pub fn reverse_fn_order(&self) -> Option<bool> {
#[expect(deprecated)]
self.metadata.as_ref().and_then(|m| m.reverse_fn_order).or(self.reverse_fn_order)
}
pub fn hidden(&self) -> bool {
self.metadata.as_ref().and_then(|m| m.auto_generated).unwrap_or(false)
}
pub fn source_path(&self) -> Option<&String> {
self.metadata.as_ref().and_then(|m| m.source_path.as_ref())
}
}
#[derive(Default, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct ScratchConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub platform: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub compiler: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub c_flags: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ctx_path: Option<PathBuf>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub build_ctx: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub preset_id: Option<u32>,
}
pub const CONFIG_FILENAMES: [&str; 3] = ["objdiff.json", "objdiff.yml", "objdiff.yaml"];
pub const DEFAULT_WATCH_PATTERNS: &[&str] = &[
"*.c", "*.cp", "*.cpp", "*.cxx", "*.h", "*.hp", "*.hpp", "*.hxx", "*.s", "*.S", "*.asm",
"*.inc", "*.py", "*.yml", "*.txt", "*.json",
];
#[derive(Clone, Eq, PartialEq)]
pub struct ProjectConfigInfo {
pub path: PathBuf,
pub timestamp: Option<FileTime>,
}
pub fn try_project_config(dir: &Path) -> Option<(Result<ProjectConfig>, ProjectConfigInfo)> {
for filename in CONFIG_FILENAMES.iter() {
let config_path = dir.join(filename);
let Ok(file) = File::open(&config_path) else {
continue;
};
let metadata = file.metadata();
if let Ok(metadata) = metadata {
if !metadata.is_file() {
continue;
}
let ts = FileTime::from_last_modification_time(&metadata);
let mut reader = BufReader::new(file);
let mut result = match filename.contains("json") {
true => read_json_config(&mut reader),
false => read_yml_config(&mut reader),
};
if let Ok(config) = &result {
// Validate min_version if present
if let Err(e) = validate_min_version(config) {
result = Err(e);
}
}
return Some((result, ProjectConfigInfo { path: config_path, timestamp: Some(ts) }));
}
}
None
}
pub fn save_project_config(
config: &ProjectConfig,
info: &ProjectConfigInfo,
) -> Result<ProjectConfigInfo> {
if let Some(last_ts) = info.timestamp {
// Check if the file has changed since we last read it
if let Ok(metadata) = fs::metadata(&info.path) {
let ts = FileTime::from_last_modification_time(&metadata);
if ts != last_ts {
return Err(anyhow!("Config file has changed since last read"));
}
}
}
let mut writer =
BufWriter::new(File::create(&info.path).context("Failed to create config file")?);
let ext = info.path.extension().and_then(|ext| ext.to_str()).unwrap_or("json");
match ext {
"json" => serde_json::to_writer_pretty(&mut writer, config).context("Failed to write JSON"),
"yml" | "yaml" => {
serde_yaml::to_writer(&mut writer, config).context("Failed to write YAML")
}
_ => Err(anyhow!("Unknown config file extension: {ext}")),
}?;
let file = writer.into_inner().context("Failed to flush file")?;
let metadata = file.metadata().context("Failed to get file metadata")?;
let ts = FileTime::from_last_modification_time(&metadata);
Ok(ProjectConfigInfo { path: info.path.clone(), timestamp: Some(ts) })
}
fn validate_min_version(config: &ProjectConfig) -> Result<()> {
let Some(min_version) = &config.min_version else { return Ok(()) };
let version = semver::Version::parse(env!("CARGO_PKG_VERSION"))
.context("Failed to parse package version")?;
let min_version = semver::Version::parse(min_version).context("Failed to parse min_version")?;
if version >= min_version {
Ok(())
} else {
Err(anyhow!("Project requires objdiff version {min_version} or higher"))
}
}
fn read_yml_config<R: Read>(reader: &mut R) -> Result<ProjectConfig> {
Ok(serde_yaml::from_reader(reader)?)
}
fn read_json_config<R: Read>(reader: &mut R) -> Result<ProjectConfig> {
Ok(serde_json::from_reader(reader)?)
}
pub fn build_globset(vec: &[Glob]) -> std::result::Result<GlobSet, globset::Error> {
let mut builder = GlobSetBuilder::new();
for glob in vec {
builder.add(glob.clone());
}
builder.build()
}

View File

@@ -0,0 +1,371 @@
use std::{cmp::max, collections::BTreeMap};
use anyhow::{anyhow, Result};
use similar::{capture_diff_slices_deadline, Algorithm};
use crate::{
arch::ProcessCodeResult,
diff::{
DiffObjConfig, ObjInsArgDiff, ObjInsBranchFrom, ObjInsBranchTo, ObjInsDiff, ObjInsDiffKind,
ObjSymbolDiff,
},
obj::{ObjInfo, ObjInsArg, ObjReloc, ObjSymbolFlags, SymbolRef},
};
pub fn process_code_symbol(
obj: &ObjInfo,
symbol_ref: SymbolRef,
config: &DiffObjConfig,
) -> Result<ProcessCodeResult> {
let (section, symbol) = obj.section_symbol(symbol_ref);
let section = section.ok_or_else(|| anyhow!("Code symbol section not found"))?;
let code = &section.data
[symbol.section_address as usize..(symbol.section_address + symbol.size) as usize];
obj.arch.process_code(
symbol.address,
code,
section.orig_index,
&section.relocations,
&section.line_info,
config,
)
}
pub fn no_diff_code(out: &ProcessCodeResult, symbol_ref: SymbolRef) -> Result<ObjSymbolDiff> {
let mut diff = Vec::<ObjInsDiff>::new();
for i in &out.insts {
diff.push(ObjInsDiff {
ins: Some(i.clone()),
kind: ObjInsDiffKind::None,
..Default::default()
});
}
resolve_branches(&mut diff);
Ok(ObjSymbolDiff { symbol_ref, target_symbol: None, instructions: diff, match_percent: None })
}
pub fn diff_code(
left_obj: &ObjInfo,
right_obj: &ObjInfo,
left_out: &ProcessCodeResult,
right_out: &ProcessCodeResult,
left_symbol_ref: SymbolRef,
right_symbol_ref: SymbolRef,
config: &DiffObjConfig,
) -> Result<(ObjSymbolDiff, ObjSymbolDiff)> {
let mut left_diff = Vec::<ObjInsDiff>::new();
let mut right_diff = Vec::<ObjInsDiff>::new();
diff_instructions(&mut left_diff, &mut right_diff, left_out, right_out)?;
resolve_branches(&mut left_diff);
resolve_branches(&mut right_diff);
let mut diff_state = InsDiffState::default();
for (left, right) in left_diff.iter_mut().zip(right_diff.iter_mut()) {
let result = compare_ins(config, left_obj, right_obj, left, right, &mut diff_state)?;
left.kind = result.kind;
right.kind = result.kind;
left.arg_diff = result.left_args_diff;
right.arg_diff = result.right_args_diff;
}
let total = left_out.insts.len().max(right_out.insts.len());
let percent = if diff_state.diff_count >= total {
0.0
} else {
((total - diff_state.diff_count) as f32 / total as f32) * 100.0
};
Ok((
ObjSymbolDiff {
symbol_ref: left_symbol_ref,
target_symbol: Some(right_symbol_ref),
instructions: left_diff,
match_percent: Some(percent),
},
ObjSymbolDiff {
symbol_ref: right_symbol_ref,
target_symbol: Some(left_symbol_ref),
instructions: right_diff,
match_percent: Some(percent),
},
))
}
fn diff_instructions(
left_diff: &mut Vec<ObjInsDiff>,
right_diff: &mut Vec<ObjInsDiff>,
left_code: &ProcessCodeResult,
right_code: &ProcessCodeResult,
) -> Result<()> {
let ops =
capture_diff_slices_deadline(Algorithm::Patience, &left_code.ops, &right_code.ops, None);
if ops.is_empty() {
left_diff.extend(
left_code
.insts
.iter()
.map(|i| ObjInsDiff { ins: Some(i.clone()), ..Default::default() }),
);
right_diff.extend(
right_code
.insts
.iter()
.map(|i| ObjInsDiff { ins: Some(i.clone()), ..Default::default() }),
);
return Ok(());
}
for op in ops {
let (_tag, left_range, right_range) = op.as_tag_tuple();
let len = max(left_range.len(), right_range.len());
left_diff.extend(
left_code.insts[left_range.clone()]
.iter()
.map(|i| ObjInsDiff { ins: Some(i.clone()), ..Default::default() }),
);
right_diff.extend(
right_code.insts[right_range.clone()]
.iter()
.map(|i| ObjInsDiff { ins: Some(i.clone()), ..Default::default() }),
);
if left_range.len() < len {
left_diff.extend((left_range.len()..len).map(|_| ObjInsDiff::default()));
}
if right_range.len() < len {
right_diff.extend((right_range.len()..len).map(|_| ObjInsDiff::default()));
}
}
Ok(())
}
fn resolve_branches(vec: &mut [ObjInsDiff]) {
let mut branch_idx = 0usize;
// Map addresses to indices
let mut addr_map = BTreeMap::<u64, usize>::new();
for (i, ins_diff) in vec.iter().enumerate() {
if let Some(ins) = &ins_diff.ins {
addr_map.insert(ins.address, i);
}
}
// Generate branches
let mut branches = BTreeMap::<usize, ObjInsBranchFrom>::new();
for (i, ins_diff) in vec.iter_mut().enumerate() {
if let Some(ins) = &ins_diff.ins {
if let Some(ins_idx) = ins.branch_dest.and_then(|a| addr_map.get(&a)) {
if let Some(branch) = branches.get_mut(ins_idx) {
ins_diff.branch_to =
Some(ObjInsBranchTo { ins_idx: *ins_idx, branch_idx: branch.branch_idx });
branch.ins_idx.push(i);
} else {
ins_diff.branch_to = Some(ObjInsBranchTo { ins_idx: *ins_idx, branch_idx });
branches.insert(*ins_idx, ObjInsBranchFrom { ins_idx: vec![i], branch_idx });
branch_idx += 1;
}
}
}
}
// Store branch from
for (i, branch) in branches {
vec[i].branch_from = Some(branch);
}
}
fn address_eq(left: &ObjReloc, right: &ObjReloc) -> bool {
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(
config: &DiffObjConfig,
left_obj: &ObjInfo,
right_obj: &ObjInfo,
left_reloc: Option<&ObjReloc>,
right_reloc: Option<&ObjReloc>,
) -> bool {
let (Some(left), Some(right)) = (left_reloc, right_reloc) else {
return false;
};
if left.flags != right.flags {
return false;
}
if config.relax_reloc_diffs {
return true;
}
let symbol_name_matches = left.target.name == right.target.name;
match (&left.target.orig_section_index, &right.target.orig_section_index) {
(Some(sl), Some(sr)) => {
// Match if section and name or address match
section_name_eq(left_obj, right_obj, *sl, *sr)
&& (symbol_name_matches || address_eq(left, right))
}
(Some(_), None) => false,
(None, Some(_)) => {
// Match if possibly stripped weak symbol
symbol_name_matches && right.target.flags.0.contains(ObjSymbolFlags::Weak)
}
(None, None) => symbol_name_matches,
}
}
fn arg_eq(
config: &DiffObjConfig,
left_obj: &ObjInfo,
right_obj: &ObjInfo,
left: &ObjInsArg,
right: &ObjInsArg,
left_diff: &ObjInsDiff,
right_diff: &ObjInsDiff,
) -> bool {
match left {
ObjInsArg::PlainText(l) => match right {
ObjInsArg::PlainText(r) => l == r,
_ => false,
},
ObjInsArg::Arg(l) => match right {
ObjInsArg::Arg(r) => l == r,
// If relocations are relaxed, match if left is a constant and right is a reloc
// Useful for instances where the target object is created without relocations
ObjInsArg::Reloc => config.relax_reloc_diffs,
_ => false,
},
ObjInsArg::Reloc => {
matches!(right, ObjInsArg::Reloc)
&& reloc_eq(
config,
left_obj,
right_obj,
left_diff.ins.as_ref().and_then(|i| i.reloc.as_ref()),
right_diff.ins.as_ref().and_then(|i| i.reloc.as_ref()),
)
}
ObjInsArg::BranchDest(_) => match right {
// Compare dest instruction idx after diffing
ObjInsArg::BranchDest(_) => {
left_diff.branch_to.as_ref().map(|b| b.ins_idx)
== right_diff.branch_to.as_ref().map(|b| b.ins_idx)
}
// If relocations are relaxed, match if left is a constant and right is a reloc
// Useful for instances where the target object is created without relocations
ObjInsArg::Reloc => config.relax_reloc_diffs,
_ => false,
},
}
}
#[derive(Default)]
struct InsDiffState {
diff_count: usize,
left_arg_idx: usize,
right_arg_idx: usize,
left_args_idx: BTreeMap<String, usize>,
right_args_idx: BTreeMap<String, usize>,
}
#[derive(Default)]
struct InsDiffResult {
kind: ObjInsDiffKind,
left_args_diff: Vec<Option<ObjInsArgDiff>>,
right_args_diff: Vec<Option<ObjInsArgDiff>>,
}
fn compare_ins(
config: &DiffObjConfig,
left_obj: &ObjInfo,
right_obj: &ObjInfo,
left: &ObjInsDiff,
right: &ObjInsDiff,
state: &mut InsDiffState,
) -> Result<InsDiffResult> {
let mut result = InsDiffResult::default();
if let (Some(left_ins), Some(right_ins)) = (&left.ins, &right.ins) {
// Count only non-PlainText args
let left_args_count = left_ins.iter_args().count();
let right_args_count = right_ins.iter_args().count();
if left_args_count != right_args_count || left_ins.op != right_ins.op {
// Totally different op
result.kind = ObjInsDiffKind::Replace;
state.diff_count += 1;
return Ok(result);
}
if left_ins.mnemonic != right_ins.mnemonic {
// Same op but different mnemonic, still cmp args
result.kind = ObjInsDiffKind::OpMismatch;
state.diff_count += 1;
}
for (a, b) in left_ins.iter_args().zip(right_ins.iter_args()) {
if arg_eq(config, left_obj, right_obj, a, b, left, right) {
result.left_args_diff.push(None);
result.right_args_diff.push(None);
} else {
if result.kind == ObjInsDiffKind::None {
result.kind = ObjInsDiffKind::ArgMismatch;
state.diff_count += 1;
}
let a_str = match a {
ObjInsArg::PlainText(arg) => arg.to_string(),
ObjInsArg::Arg(arg) => arg.to_string(),
ObjInsArg::Reloc => left_ins
.reloc
.as_ref()
.map_or_else(|| "<unknown>".to_string(), |r| r.target.name.clone()),
ObjInsArg::BranchDest(arg) => arg.to_string(),
};
let a_diff = if let Some(idx) = state.left_args_idx.get(&a_str) {
ObjInsArgDiff { idx: *idx }
} else {
let idx = state.left_arg_idx;
state.left_args_idx.insert(a_str, idx);
state.left_arg_idx += 1;
ObjInsArgDiff { idx }
};
let b_str = match b {
ObjInsArg::PlainText(arg) => arg.to_string(),
ObjInsArg::Arg(arg) => arg.to_string(),
ObjInsArg::Reloc => right_ins
.reloc
.as_ref()
.map_or_else(|| "<unknown>".to_string(), |r| r.target.name.clone()),
ObjInsArg::BranchDest(arg) => arg.to_string(),
};
let b_diff = if let Some(idx) = state.right_args_idx.get(&b_str) {
ObjInsArgDiff { idx: *idx }
} else {
let idx = state.right_arg_idx;
state.right_args_idx.insert(b_str, idx);
state.right_arg_idx += 1;
ObjInsArgDiff { idx }
};
result.left_args_diff.push(Some(a_diff));
result.right_args_diff.push(Some(b_diff));
}
}
} else if left.ins.is_some() {
result.kind = ObjInsDiffKind::Delete;
state.diff_count += 1;
} else {
result.kind = ObjInsDiffKind::Insert;
state.diff_count += 1;
}
Ok(result)
}

View File

@@ -0,0 +1,222 @@
use std::cmp::{max, min, Ordering};
use anyhow::{anyhow, Result};
use similar::{capture_diff_slices_deadline, get_diff_ratio, Algorithm};
use crate::{
diff::{ObjDataDiff, ObjDataDiffKind, ObjSectionDiff, ObjSymbolDiff},
obj::{ObjInfo, ObjSection, SymbolRef},
};
pub fn diff_bss_symbol(
left_obj: &ObjInfo,
right_obj: &ObjInfo,
left_symbol_ref: SymbolRef,
right_symbol_ref: SymbolRef,
) -> Result<(ObjSymbolDiff, ObjSymbolDiff)> {
let (_, left_symbol) = left_obj.section_symbol(left_symbol_ref);
let (_, right_symbol) = right_obj.section_symbol(right_symbol_ref);
let percent = if left_symbol.size == right_symbol.size { 100.0 } else { 50.0 };
Ok((
ObjSymbolDiff {
symbol_ref: left_symbol_ref,
target_symbol: Some(right_symbol_ref),
instructions: vec![],
match_percent: Some(percent),
},
ObjSymbolDiff {
symbol_ref: right_symbol_ref,
target_symbol: Some(left_symbol_ref),
instructions: vec![],
match_percent: Some(percent),
},
))
}
pub fn no_diff_symbol(_obj: &ObjInfo, symbol_ref: SymbolRef) -> ObjSymbolDiff {
ObjSymbolDiff { symbol_ref, target_symbol: None, instructions: vec![], match_percent: None }
}
/// Compare the data sections of two object files.
pub fn diff_data_section(
left: &ObjSection,
right: &ObjSection,
left_section_diff: &ObjSectionDiff,
right_section_diff: &ObjSectionDiff,
) -> Result<(ObjSectionDiff, ObjSectionDiff)> {
let left_max =
left.symbols.iter().map(|s| s.section_address + s.size).max().unwrap_or(0).min(left.size);
let right_max =
right.symbols.iter().map(|s| s.section_address + s.size).max().unwrap_or(0).min(right.size);
let left_data = &left.data[..left_max as usize];
let right_data = &right.data[..right_max as usize];
let ops = capture_diff_slices_deadline(Algorithm::Patience, left_data, right_data, None);
let match_percent = get_diff_ratio(&ops, left_data.len(), right_data.len()) * 100.0;
let mut left_diff = Vec::<ObjDataDiff>::new();
let mut right_diff = Vec::<ObjDataDiff>::new();
for op in ops {
let (tag, left_range, right_range) = op.as_tag_tuple();
let left_len = left_range.len();
let right_len = right_range.len();
let mut len = max(left_len, right_len);
let kind = match tag {
similar::DiffTag::Equal => ObjDataDiffKind::None,
similar::DiffTag::Delete => ObjDataDiffKind::Delete,
similar::DiffTag::Insert => ObjDataDiffKind::Insert,
similar::DiffTag::Replace => {
// Ensure replacements are equal length
len = min(left_len, right_len);
ObjDataDiffKind::Replace
}
};
let left_data = &left.data[left_range];
let right_data = &right.data[right_range];
left_diff.push(ObjDataDiff {
data: left_data[..min(len, left_data.len())].to_vec(),
kind,
len,
..Default::default()
});
right_diff.push(ObjDataDiff {
data: right_data[..min(len, right_data.len())].to_vec(),
kind,
len,
..Default::default()
});
if kind == ObjDataDiffKind::Replace {
match left_len.cmp(&right_len) {
Ordering::Less => {
let len = right_len - left_len;
left_diff.push(ObjDataDiff {
data: vec![],
kind: ObjDataDiffKind::Insert,
len,
..Default::default()
});
right_diff.push(ObjDataDiff {
data: right_data[left_len..right_len].to_vec(),
kind: ObjDataDiffKind::Insert,
len,
..Default::default()
});
}
Ordering::Greater => {
let len = left_len - right_len;
left_diff.push(ObjDataDiff {
data: left_data[right_len..left_len].to_vec(),
kind: ObjDataDiffKind::Delete,
len,
..Default::default()
});
right_diff.push(ObjDataDiff {
data: vec![],
kind: ObjDataDiffKind::Delete,
len,
..Default::default()
});
}
Ordering::Equal => {}
}
}
}
let (mut left_section_diff, mut right_section_diff) =
diff_generic_section(left, right, left_section_diff, right_section_diff)?;
left_section_diff.data_diff = left_diff;
right_section_diff.data_diff = right_diff;
// Use the highest match percent between two options:
// - Left symbols matching right symbols by name
// - Diff of the data itself
if left_section_diff.match_percent.unwrap_or(-1.0) < match_percent {
left_section_diff.match_percent = Some(match_percent);
right_section_diff.match_percent = Some(match_percent);
}
Ok((left_section_diff, right_section_diff))
}
pub fn diff_data_symbol(
left_obj: &ObjInfo,
right_obj: &ObjInfo,
left_symbol_ref: SymbolRef,
right_symbol_ref: SymbolRef,
) -> Result<(ObjSymbolDiff, ObjSymbolDiff)> {
let (left_section, left_symbol) = left_obj.section_symbol(left_symbol_ref);
let (right_section, right_symbol) = right_obj.section_symbol(right_symbol_ref);
let left_section = left_section.ok_or_else(|| anyhow!("Data symbol section not found"))?;
let right_section = right_section.ok_or_else(|| anyhow!("Data symbol section not found"))?;
let left_data = &left_section.data[left_symbol.section_address as usize
..(left_symbol.section_address + left_symbol.size) as usize];
let right_data = &right_section.data[right_symbol.section_address as usize
..(right_symbol.section_address + right_symbol.size) as usize];
let ops = capture_diff_slices_deadline(Algorithm::Patience, left_data, right_data, None);
let match_percent = get_diff_ratio(&ops, left_data.len(), right_data.len()) * 100.0;
Ok((
ObjSymbolDiff {
symbol_ref: left_symbol_ref,
target_symbol: Some(right_symbol_ref),
instructions: vec![],
match_percent: Some(match_percent),
},
ObjSymbolDiff {
symbol_ref: right_symbol_ref,
target_symbol: Some(left_symbol_ref),
instructions: vec![],
match_percent: Some(match_percent),
},
))
}
/// Compares a section of two object files.
/// This essentially adds up the match percentage of each symbol in the section.
pub fn diff_generic_section(
left: &ObjSection,
_right: &ObjSection,
left_diff: &ObjSectionDiff,
_right_diff: &ObjSectionDiff,
) -> Result<(ObjSectionDiff, ObjSectionDiff)> {
let match_percent = if left_diff.symbols.iter().all(|d| d.match_percent == Some(100.0)) {
100.0 // Avoid fp precision issues
} else {
left.symbols
.iter()
.zip(left_diff.symbols.iter())
.map(|(s, d)| d.match_percent.unwrap_or(0.0) * s.size as f32)
.sum::<f32>()
/ left.size as f32
};
Ok((
ObjSectionDiff { symbols: vec![], data_diff: vec![], match_percent: Some(match_percent) },
ObjSectionDiff { symbols: vec![], data_diff: vec![], match_percent: Some(match_percent) },
))
}
/// Compare the addresses and sizes of each symbol in the BSS sections.
pub fn diff_bss_section(
left: &ObjSection,
right: &ObjSection,
left_diff: &ObjSectionDiff,
right_diff: &ObjSectionDiff,
) -> Result<(ObjSectionDiff, ObjSectionDiff)> {
let left_sizes = left.symbols.iter().map(|s| (s.section_address, s.size)).collect::<Vec<_>>();
let right_sizes = right.symbols.iter().map(|s| (s.section_address, s.size)).collect::<Vec<_>>();
let ops = capture_diff_slices_deadline(Algorithm::Patience, &left_sizes, &right_sizes, None);
let mut match_percent = get_diff_ratio(&ops, left_sizes.len(), right_sizes.len()) * 100.0;
// Use the highest match percent between two options:
// - Left symbols matching right symbols by name
// - Diff of the addresses and sizes of each symbol
let (generic_diff, _) = diff_generic_section(left, right, left_diff, right_diff)?;
if generic_diff.match_percent.unwrap_or(-1.0) > match_percent {
match_percent = generic_diff.match_percent.unwrap();
}
Ok((
ObjSectionDiff { symbols: vec![], data_diff: vec![], match_percent: Some(match_percent) },
ObjSectionDiff { symbols: vec![], data_diff: vec![], match_percent: Some(match_percent) },
))
}

View File

@@ -0,0 +1,137 @@
use std::cmp::Ordering;
use crate::{
diff::{ObjInsArgDiff, ObjInsDiff},
obj::{ObjInsArg, ObjInsArgValue, ObjReloc, ObjSymbol},
};
#[derive(Debug, Copy, Clone)]
pub enum DiffText<'a> {
/// Basic text
Basic(&'a str),
/// Colored text
BasicColor(&'a str, usize),
/// Line number
Line(u32),
/// Instruction address
Address(u64),
/// Instruction mnemonic
Opcode(&'a str, u16),
/// Instruction argument
Argument(&'a ObjInsArgValue, Option<&'a ObjInsArgDiff>),
/// Branch destination
BranchDest(u64, Option<&'a ObjInsArgDiff>),
/// Symbol name
Symbol(&'a ObjSymbol, Option<&'a ObjInsArgDiff>),
/// Number of spaces
Spacing(usize),
/// End of line
Eol,
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub enum HighlightKind {
#[default]
None,
Opcode(u16),
Arg(ObjInsArgValue),
Symbol(String),
Address(u64),
}
pub fn display_diff<E>(
ins_diff: &ObjInsDiff,
base_addr: u64,
mut cb: impl FnMut(DiffText) -> Result<(), E>,
) -> Result<(), E> {
let Some(ins) = &ins_diff.ins else {
cb(DiffText::Eol)?;
return Ok(());
};
if let Some(line) = ins.line {
cb(DiffText::Line(line))?;
}
cb(DiffText::Address(ins.address - base_addr))?;
if let Some(branch) = &ins_diff.branch_from {
cb(DiffText::BasicColor(" ~> ", branch.branch_idx))?;
} else {
cb(DiffText::Spacing(4))?;
}
cb(DiffText::Opcode(&ins.mnemonic, ins.op))?;
let mut arg_diff_idx = 0; // non-PlainText index
for (i, arg) in ins.args.iter().enumerate() {
if i == 0 {
cb(DiffText::Spacing(1))?;
}
let diff = ins_diff.arg_diff.get(arg_diff_idx).and_then(|o| o.as_ref());
match arg {
ObjInsArg::PlainText(s) => {
cb(DiffText::Basic(s))?;
}
ObjInsArg::Arg(v) => {
cb(DiffText::Argument(v, diff))?;
arg_diff_idx += 1;
}
ObjInsArg::Reloc => {
display_reloc_name(ins.reloc.as_ref().unwrap(), &mut cb, diff)?;
arg_diff_idx += 1;
}
ObjInsArg::BranchDest(dest) => {
if let Some(dest) = dest.checked_sub(base_addr) {
cb(DiffText::BranchDest(dest, diff))?;
} else {
cb(DiffText::Basic("<unknown>"))?;
}
arg_diff_idx += 1;
}
}
}
if let Some(branch) = &ins_diff.branch_to {
cb(DiffText::BasicColor(" ~>", branch.branch_idx))?;
}
cb(DiffText::Eol)?;
Ok(())
}
fn display_reloc_name<E>(
reloc: &ObjReloc,
mut cb: impl FnMut(DiffText) -> Result<(), E>,
diff: Option<&ObjInsArgDiff>,
) -> Result<(), E> {
cb(DiffText::Symbol(&reloc.target, diff))?;
match reloc.addend.cmp(&0i64) {
Ordering::Greater => cb(DiffText::Basic(&format!("+{:#x}", reloc.addend))),
Ordering::Less => cb(DiffText::Basic(&format!("-{:#x}", -reloc.addend))),
_ => Ok(()),
}
}
impl PartialEq<DiffText<'_>> for HighlightKind {
fn eq(&self, other: &DiffText) -> bool {
match (self, other) {
(HighlightKind::Opcode(a), DiffText::Opcode(_, b)) => a == b,
(HighlightKind::Arg(a), DiffText::Argument(b, _)) => a.loose_eq(b),
(HighlightKind::Symbol(a), DiffText::Symbol(b, _)) => a == &b.name,
(HighlightKind::Address(a), DiffText::Address(b) | DiffText::BranchDest(b, _)) => {
a == b
}
_ => false,
}
}
}
impl PartialEq<HighlightKind> for DiffText<'_> {
fn eq(&self, other: &HighlightKind) -> bool { other.eq(self) }
}
impl From<DiffText<'_>> for HighlightKind {
fn from(value: DiffText<'_>) -> Self {
match value {
DiffText::Opcode(_, op) => HighlightKind::Opcode(op),
DiffText::Argument(arg, _) => HighlightKind::Arg(arg.clone()),
DiffText::Symbol(sym, _) => HighlightKind::Symbol(sym.name.to_string()),
DiffText::Address(addr) | DiffText::BranchDest(addr, _) => HighlightKind::Address(addr),
_ => HighlightKind::None,
}
}
}

View File

@@ -0,0 +1,927 @@
use std::collections::HashSet;
use anyhow::Result;
use crate::{
config::SymbolMappings,
diff::{
code::{diff_code, no_diff_code, process_code_symbol},
data::{
diff_bss_section, diff_bss_symbol, diff_data_section, diff_data_symbol,
diff_generic_section, no_diff_symbol,
},
},
obj::{ObjInfo, ObjIns, ObjSection, ObjSectionKind, ObjSymbol, SymbolRef, SECTION_COMMON},
};
pub mod code;
pub mod data;
pub mod display;
#[derive(
Debug,
Copy,
Clone,
Default,
Eq,
PartialEq,
serde::Deserialize,
serde::Serialize,
strum::VariantArray,
strum::EnumMessage,
)]
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
pub enum X86Formatter {
#[default]
#[strum(message = "Intel (default)")]
Intel,
#[strum(message = "AT&T")]
Gas,
#[strum(message = "NASM")]
Nasm,
#[strum(message = "MASM")]
Masm,
}
#[derive(
Debug,
Copy,
Clone,
Default,
Eq,
PartialEq,
serde::Deserialize,
serde::Serialize,
strum::VariantArray,
strum::EnumMessage,
)]
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
pub enum MipsAbi {
#[default]
#[strum(message = "Auto (default)")]
Auto,
#[strum(message = "O32")]
O32,
#[strum(message = "N32")]
N32,
#[strum(message = "N64")]
N64,
}
#[derive(
Debug,
Copy,
Clone,
Default,
Eq,
PartialEq,
serde::Deserialize,
serde::Serialize,
strum::VariantArray,
strum::EnumMessage,
)]
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
pub enum MipsInstrCategory {
#[default]
#[strum(message = "Auto (default)")]
Auto,
#[strum(message = "CPU")]
Cpu,
#[strum(message = "RSP (N64)")]
Rsp,
#[strum(message = "R3000 GTE (PS1)")]
R3000Gte,
#[strum(message = "R4000 ALLEGREX (PSP)")]
R4000Allegrex,
#[strum(message = "R5900 EE (PS2)")]
R5900,
}
#[derive(
Debug,
Copy,
Clone,
Default,
Eq,
PartialEq,
serde::Deserialize,
serde::Serialize,
strum::VariantArray,
strum::EnumMessage,
)]
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
pub enum ArmArchVersion {
#[default]
#[strum(message = "Auto (default)")]
Auto,
#[strum(message = "ARMv4T (GBA)")]
V4T,
#[strum(message = "ARMv5TE (DS)")]
V5TE,
#[strum(message = "ARMv6K (3DS)")]
V6K,
}
#[derive(
Debug,
Copy,
Clone,
Default,
Eq,
PartialEq,
serde::Deserialize,
serde::Serialize,
strum::VariantArray,
strum::EnumMessage,
)]
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
pub enum ArmR9Usage {
#[default]
#[strum(
message = "R9 or V6 (default)",
detailed_message = "Use R9 as a general-purpose register."
)]
GeneralPurpose,
#[strum(
message = "SB (static base)",
detailed_message = "Used for position-independent data (PID)."
)]
Sb,
#[strum(message = "TR (TLS register)", detailed_message = "Used for thread-local storage.")]
Tr,
}
#[inline]
const fn default_true() -> bool { true }
#[derive(Debug, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
#[cfg_attr(feature = "wasm", tsify(from_wasm_abi))]
#[serde(default)]
pub struct DiffObjConfig {
pub relax_reloc_diffs: bool,
#[serde(default = "default_true")]
pub space_between_args: bool,
pub combine_data_sections: bool,
#[serde(default)]
pub symbol_mappings: MappingConfig,
// x86
pub x86_formatter: X86Formatter,
// MIPS
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 DiffObjConfig {
fn default() -> Self {
Self {
relax_reloc_diffs: false,
space_between_args: true,
combine_data_sections: false,
symbol_mappings: Default::default(),
x86_formatter: Default::default(),
mips_abi: Default::default(),
mips_instr_category: Default::default(),
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 DiffObjConfig {
pub fn separator(&self) -> &'static str {
if self.space_between_args {
", "
} else {
","
}
}
}
#[derive(Debug, Clone)]
pub struct ObjSectionDiff {
pub symbols: Vec<ObjSymbolDiff>,
pub data_diff: Vec<ObjDataDiff>,
pub match_percent: Option<f32>,
}
impl ObjSectionDiff {
fn merge(&mut self, other: ObjSectionDiff) {
// symbols ignored
self.data_diff = other.data_diff;
self.match_percent = other.match_percent;
}
}
#[derive(Debug, Clone, Default)]
pub struct ObjSymbolDiff {
/// The symbol ref this object
pub symbol_ref: SymbolRef,
/// The symbol ref in the _other_ object that this symbol was diffed against
pub target_symbol: Option<SymbolRef>,
pub instructions: Vec<ObjInsDiff>,
pub match_percent: Option<f32>,
}
#[derive(Debug, Clone, Default)]
pub struct ObjInsDiff {
pub ins: Option<ObjIns>,
/// Diff kind
pub kind: ObjInsDiffKind,
/// Branches from instruction
pub branch_from: Option<ObjInsBranchFrom>,
/// Branches to instruction
pub branch_to: Option<ObjInsBranchTo>,
/// Arg diffs (only contains non-PlainText args)
pub arg_diff: Vec<Option<ObjInsArgDiff>>,
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
pub enum ObjInsDiffKind {
#[default]
None,
OpMismatch,
ArgMismatch,
Replace,
Delete,
Insert,
}
#[derive(Debug, Clone, Default)]
pub struct ObjDataDiff {
pub data: Vec<u8>,
pub kind: ObjDataDiffKind,
pub len: usize,
pub symbol: String,
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
pub enum ObjDataDiffKind {
#[default]
None,
Replace,
Delete,
Insert,
}
#[derive(Debug, Copy, Clone)]
pub struct ObjInsArgDiff {
/// Incrementing index for coloring
pub idx: usize,
}
#[derive(Debug, Clone)]
pub struct ObjInsBranchFrom {
/// Source instruction indices
pub ins_idx: Vec<usize>,
/// Incrementing index for coloring
pub branch_idx: usize,
}
#[derive(Debug, Clone)]
pub struct ObjInsBranchTo {
/// Target instruction index
pub ins_idx: usize,
/// Incrementing index for coloring
pub branch_idx: usize,
}
#[derive(Default)]
pub struct ObjDiff {
/// A list of all section diffs in the object.
pub sections: Vec<ObjSectionDiff>,
/// Common BSS symbols don't live in a section, so they're stored separately.
pub common: Vec<ObjSymbolDiff>,
/// If `selecting_left` or `selecting_right` is set, this is the list of symbols
/// that are being mapped to the other object.
pub mapping_symbols: Vec<ObjSymbolDiff>,
}
impl ObjDiff {
pub fn new_from_obj(obj: &ObjInfo) -> Self {
let mut result = Self {
sections: Vec::with_capacity(obj.sections.len()),
common: Vec::with_capacity(obj.common.len()),
mapping_symbols: vec![],
};
for (section_idx, section) in obj.sections.iter().enumerate() {
let mut symbols = Vec::with_capacity(section.symbols.len());
for (symbol_idx, _) in section.symbols.iter().enumerate() {
symbols.push(ObjSymbolDiff {
symbol_ref: SymbolRef { section_idx, symbol_idx },
target_symbol: None,
instructions: vec![],
match_percent: None,
});
}
result.sections.push(ObjSectionDiff {
symbols,
data_diff: vec![ObjDataDiff {
data: section.data.clone(),
kind: ObjDataDiffKind::None,
len: section.data.len(),
symbol: section.name.clone(),
}],
match_percent: None,
});
}
for (symbol_idx, _) in obj.common.iter().enumerate() {
result.common.push(ObjSymbolDiff {
symbol_ref: SymbolRef { section_idx: SECTION_COMMON, symbol_idx },
target_symbol: None,
instructions: vec![],
match_percent: None,
});
}
result
}
#[inline]
pub fn section_diff(&self, section_idx: usize) -> &ObjSectionDiff {
&self.sections[section_idx]
}
#[inline]
pub fn section_diff_mut(&mut self, section_idx: usize) -> &mut ObjSectionDiff {
&mut self.sections[section_idx]
}
#[inline]
pub fn symbol_diff(&self, symbol_ref: SymbolRef) -> &ObjSymbolDiff {
if symbol_ref.section_idx == SECTION_COMMON {
&self.common[symbol_ref.symbol_idx]
} else {
&self.section_diff(symbol_ref.section_idx).symbols[symbol_ref.symbol_idx]
}
}
#[inline]
pub fn symbol_diff_mut(&mut self, symbol_ref: SymbolRef) -> &mut ObjSymbolDiff {
if symbol_ref.section_idx == SECTION_COMMON {
&mut self.common[symbol_ref.symbol_idx]
} else {
&mut self.section_diff_mut(symbol_ref.section_idx).symbols[symbol_ref.symbol_idx]
}
}
}
#[derive(Default)]
pub struct DiffObjsResult {
pub left: Option<ObjDiff>,
pub right: Option<ObjDiff>,
pub prev: Option<ObjDiff>,
}
pub fn diff_objs(
config: &DiffObjConfig,
left: Option<&ObjInfo>,
right: Option<&ObjInfo>,
prev: Option<&ObjInfo>,
) -> Result<DiffObjsResult> {
let symbol_matches = matching_symbols(left, right, prev, &config.symbol_mappings)?;
let section_matches = matching_sections(left, right)?;
let mut left = left.map(|p| (p, ObjDiff::new_from_obj(p)));
let mut right = right.map(|p| (p, ObjDiff::new_from_obj(p)));
let mut prev = prev.map(|p| (p, ObjDiff::new_from_obj(p)));
for symbol_match in symbol_matches {
match symbol_match {
SymbolMatch {
left: Some(left_symbol_ref),
right: Some(right_symbol_ref),
prev: prev_symbol_ref,
section_kind,
} => {
let (left_obj, left_out) = left.as_mut().unwrap();
let (right_obj, right_out) = right.as_mut().unwrap();
match section_kind {
ObjSectionKind::Code => {
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 (left_diff, right_diff) = diff_code(
left_obj,
right_obj,
&left_code,
&right_code,
left_symbol_ref,
right_symbol_ref,
config,
)?;
*left_out.symbol_diff_mut(left_symbol_ref) = left_diff;
*right_out.symbol_diff_mut(right_symbol_ref) = right_diff;
if let Some(prev_symbol_ref) = prev_symbol_ref {
let (prev_obj, prev_out) = prev.as_mut().unwrap();
let prev_code = process_code_symbol(prev_obj, prev_symbol_ref, config)?;
let (_, prev_diff) = diff_code(
left_obj,
right_obj,
&right_code,
&prev_code,
right_symbol_ref,
prev_symbol_ref,
config,
)?;
*prev_out.symbol_diff_mut(prev_symbol_ref) = prev_diff;
}
}
ObjSectionKind::Data => {
let (left_diff, right_diff) = diff_data_symbol(
left_obj,
right_obj,
left_symbol_ref,
right_symbol_ref,
)?;
*left_out.symbol_diff_mut(left_symbol_ref) = left_diff;
*right_out.symbol_diff_mut(right_symbol_ref) = right_diff;
}
ObjSectionKind::Bss => {
let (left_diff, right_diff) = diff_bss_symbol(
left_obj,
right_obj,
left_symbol_ref,
right_symbol_ref,
)?;
*left_out.symbol_diff_mut(left_symbol_ref) = left_diff;
*right_out.symbol_diff_mut(right_symbol_ref) = right_diff;
}
}
}
SymbolMatch { left: Some(left_symbol_ref), right: None, prev: _, section_kind } => {
let (left_obj, left_out) = left.as_mut().unwrap();
match section_kind {
ObjSectionKind::Code => {
let code = process_code_symbol(left_obj, left_symbol_ref, config)?;
*left_out.symbol_diff_mut(left_symbol_ref) =
no_diff_code(&code, left_symbol_ref)?;
}
ObjSectionKind::Data | ObjSectionKind::Bss => {
*left_out.symbol_diff_mut(left_symbol_ref) =
no_diff_symbol(left_obj, left_symbol_ref);
}
}
}
SymbolMatch { left: None, right: Some(right_symbol_ref), prev: _, section_kind } => {
let (right_obj, right_out) = right.as_mut().unwrap();
match section_kind {
ObjSectionKind::Code => {
let code = process_code_symbol(right_obj, right_symbol_ref, config)?;
*right_out.symbol_diff_mut(right_symbol_ref) =
no_diff_code(&code, right_symbol_ref)?;
}
ObjSectionKind::Data | ObjSectionKind::Bss => {
*right_out.symbol_diff_mut(right_symbol_ref) =
no_diff_symbol(right_obj, right_symbol_ref);
}
}
}
SymbolMatch { left: None, right: None, .. } => {
// Should not happen
}
}
}
for section_match in section_matches {
if let SectionMatch {
left: Some(left_section_idx),
right: Some(right_section_idx),
section_kind,
} = section_match
{
let (left_obj, left_out) = left.as_mut().unwrap();
let (right_obj, right_out) = right.as_mut().unwrap();
let left_section = &left_obj.sections[left_section_idx];
let right_section = &right_obj.sections[right_section_idx];
match section_kind {
ObjSectionKind::Code => {
let left_section_diff = left_out.section_diff(left_section_idx);
let right_section_diff = right_out.section_diff(right_section_idx);
let (left_diff, right_diff) = diff_generic_section(
left_section,
right_section,
left_section_diff,
right_section_diff,
)?;
left_out.section_diff_mut(left_section_idx).merge(left_diff);
right_out.section_diff_mut(right_section_idx).merge(right_diff);
}
ObjSectionKind::Data => {
let left_section_diff = left_out.section_diff(left_section_idx);
let right_section_diff = right_out.section_diff(right_section_idx);
let (left_diff, right_diff) = diff_data_section(
left_section,
right_section,
left_section_diff,
right_section_diff,
)?;
left_out.section_diff_mut(left_section_idx).merge(left_diff);
right_out.section_diff_mut(right_section_idx).merge(right_diff);
}
ObjSectionKind::Bss => {
let left_section_diff = left_out.section_diff(left_section_idx);
let right_section_diff = right_out.section_diff(right_section_idx);
let (left_diff, right_diff) = diff_bss_section(
left_section,
right_section,
left_section_diff,
right_section_diff,
)?;
left_out.section_diff_mut(left_section_idx).merge(left_diff);
right_out.section_diff_mut(right_section_idx).merge(right_diff);
}
}
}
}
if let (Some((right_obj, right_out)), Some((left_obj, left_out))) =
(right.as_mut(), left.as_mut())
{
if let Some(right_name) = &config.symbol_mappings.selecting_left {
generate_mapping_symbols(right_obj, right_name, left_obj, left_out, config)?;
}
if let Some(left_name) = &config.symbol_mappings.selecting_right {
generate_mapping_symbols(left_obj, left_name, right_obj, right_out, config)?;
}
}
Ok(DiffObjsResult {
left: left.map(|(_, o)| o),
right: right.map(|(_, o)| o),
prev: prev.map(|(_, o)| o),
})
}
/// 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() {
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)]
struct SymbolMatch {
left: Option<SymbolRef>,
right: Option<SymbolRef>,
prev: Option<SymbolRef>,
section_kind: ObjSectionKind,
}
#[derive(Copy, Clone, Eq, PartialEq)]
struct SectionMatch {
left: Option<usize>,
right: Option<usize>,
section_kind: ObjSectionKind,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, serde::Deserialize, serde::Serialize)]
pub struct MappingConfig {
/// Manual symbol mappings
pub mappings: SymbolMappings,
/// The right object symbol name that we're selecting a left symbol for
pub selecting_left: Option<String>,
/// The left object symbol name that we're selecting a right symbol for
pub selecting_right: Option<String>,
}
fn symbol_ref_by_name(obj: &ObjInfo, name: &str) -> Option<SymbolRef> {
for (section_idx, section) in obj.sections.iter().enumerate() {
for (symbol_idx, symbol) in section.symbols.iter().enumerate() {
if symbol.name == name {
return Some(SymbolRef { section_idx, symbol_idx });
}
}
}
None
}
fn apply_symbol_mappings(
left: &ObjInfo,
right: &ObjInfo,
mapping_config: &MappingConfig,
left_used: &mut HashSet<SymbolRef>,
right_used: &mut HashSet<SymbolRef>,
matches: &mut Vec<SymbolMatch>,
) -> Result<()> {
// If we're selecting a symbol to use as a comparison, mark it as used
// This ensures that we don't match it to another symbol at any point
if let Some(left_name) = &mapping_config.selecting_left {
if let Some(left_symbol) = symbol_ref_by_name(left, left_name) {
left_used.insert(left_symbol);
}
}
if let Some(right_name) = &mapping_config.selecting_right {
if let Some(right_symbol) = symbol_ref_by_name(right, right_name) {
right_used.insert(right_symbol);
}
}
// Apply manual symbol mappings
for (left_name, right_name) in &mapping_config.mappings {
let Some(left_symbol) = symbol_ref_by_name(left, left_name) else {
continue;
};
if left_used.contains(&left_symbol) {
continue;
}
let Some(right_symbol) = symbol_ref_by_name(right, right_name) else {
continue;
};
if right_used.contains(&right_symbol) {
continue;
}
let left_section = &left.sections[left_symbol.section_idx];
let right_section = &right.sections[right_symbol.section_idx];
if left_section.kind != right_section.kind {
log::warn!(
"Symbol section kind mismatch: {} ({:?}) vs {} ({:?})",
left_name,
left_section.kind,
right_name,
right_section.kind
);
continue;
}
matches.push(SymbolMatch {
left: Some(left_symbol),
right: Some(right_symbol),
prev: None, // TODO
section_kind: left_section.kind,
});
left_used.insert(left_symbol);
right_used.insert(right_symbol);
}
Ok(())
}
/// Find matching symbols between each object.
fn matching_symbols(
left: Option<&ObjInfo>,
right: Option<&ObjInfo>,
prev: Option<&ObjInfo>,
mappings: &MappingConfig,
) -> Result<Vec<SymbolMatch>> {
let mut matches = Vec::new();
let mut left_used = HashSet::new();
let mut right_used = HashSet::new();
if let Some(left) = left {
if let Some(right) = right {
apply_symbol_mappings(
left,
right,
mappings,
&mut left_used,
&mut right_used,
&mut matches,
)?;
}
for (section_idx, section) in left.sections.iter().enumerate() {
for (symbol_idx, symbol) in section.symbols.iter().enumerate() {
let symbol_ref = SymbolRef { section_idx, symbol_idx };
if left_used.contains(&symbol_ref) {
continue;
}
let symbol_match = SymbolMatch {
left: Some(symbol_ref),
right: find_symbol(right, symbol, section, Some(&right_used)),
prev: find_symbol(prev, symbol, section, None),
section_kind: section.kind,
};
matches.push(symbol_match);
if let Some(right) = symbol_match.right {
right_used.insert(right);
}
}
}
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 {
left: Some(symbol_ref),
right: find_common_symbol(right, symbol),
prev: find_common_symbol(prev, symbol),
section_kind: ObjSectionKind::Bss,
};
matches.push(symbol_match);
if let Some(right) = symbol_match.right {
right_used.insert(right);
}
}
}
if let Some(right) = right {
for (section_idx, section) in right.sections.iter().enumerate() {
for (symbol_idx, symbol) in section.symbols.iter().enumerate() {
let symbol_ref = SymbolRef { section_idx, symbol_idx };
if right_used.contains(&symbol_ref) {
continue;
}
matches.push(SymbolMatch {
left: None,
right: Some(symbol_ref),
prev: find_symbol(prev, symbol, section, None),
section_kind: section.kind,
});
}
}
for (symbol_idx, symbol) in right.common.iter().enumerate() {
let symbol_ref = SymbolRef { section_idx: SECTION_COMMON, symbol_idx };
if right_used.contains(&symbol_ref) {
continue;
}
matches.push(SymbolMatch {
left: None,
right: Some(symbol_ref),
prev: find_common_symbol(prev, symbol),
section_kind: ObjSectionKind::Bss,
});
}
}
Ok(matches)
}
fn unmatched_symbols<'section, 'used>(
section: &'section ObjSection,
section_idx: usize,
used: Option<&'used HashSet<SymbolRef>>,
) -> impl Iterator<Item = (usize, &'section ObjSymbol)> + 'used
where
'section: 'used,
{
section.symbols.iter().enumerate().filter(move |&(symbol_idx, _)| {
// Skip symbols that have already been matched
!used.map(|u| u.contains(&SymbolRef { section_idx, symbol_idx })).unwrap_or(false)
})
}
fn find_symbol(
obj: Option<&ObjInfo>,
in_symbol: &ObjSymbol,
in_section: &ObjSection,
used: Option<&HashSet<SymbolRef>>,
) -> Option<SymbolRef> {
let obj = obj?;
// Try to find an exact name match
for (section_idx, section) in obj.sections.iter().enumerate() {
if section.kind != in_section.kind {
continue;
}
if let Some((symbol_idx, _)) = unmatched_symbols(section, section_idx, used)
.find(|(_, symbol)| symbol.name == in_symbol.name)
{
return Some(SymbolRef { section_idx, symbol_idx });
}
}
// Match compiler-generated symbols against each other (e.g. @251 -> @60)
// If they are at the same address in the same section
if in_symbol.name.starts_with('@')
&& matches!(in_section.kind, ObjSectionKind::Data | ObjSectionKind::Bss)
{
if let Some((section_idx, section)) =
obj.sections.iter().enumerate().find(|(_, s)| s.name == in_section.name)
{
if let Some((symbol_idx, _)) =
unmatched_symbols(section, section_idx, used).find(|(_, symbol)| {
symbol.address == in_symbol.address && symbol.name.starts_with('@')
})
{
return Some(SymbolRef { section_idx, symbol_idx });
}
}
}
// Match Metrowerks symbol$1234 against symbol$2345
if let Some((prefix, suffix)) = in_symbol.name.split_once('$') {
if !suffix.chars().all(char::is_numeric) {
return None;
}
for (section_idx, section) in obj.sections.iter().enumerate() {
if section.kind != in_section.kind {
continue;
}
if let Some((symbol_idx, _)) =
unmatched_symbols(section, section_idx, used).find(|&(_, symbol)| {
if let Some((p, s)) = symbol.name.split_once('$') {
prefix == p && s.chars().all(char::is_numeric)
} else {
false
}
})
{
return Some(SymbolRef { section_idx, symbol_idx });
}
}
}
None
}
fn find_common_symbol(obj: Option<&ObjInfo>, in_symbol: &ObjSymbol) -> Option<SymbolRef> {
let obj = obj?;
for (symbol_idx, symbol) in obj.common.iter().enumerate() {
if symbol.name == in_symbol.name {
return Some(SymbolRef { section_idx: SECTION_COMMON, symbol_idx });
}
}
None
}
/// Find matching sections between each object.
fn matching_sections(left: Option<&ObjInfo>, right: Option<&ObjInfo>) -> Result<Vec<SectionMatch>> {
let mut matches = Vec::new();
if let Some(left) = left {
for (section_idx, section) in left.sections.iter().enumerate() {
matches.push(SectionMatch {
left: Some(section_idx),
right: find_section(right, &section.name, section.kind),
section_kind: section.kind,
});
}
}
if let Some(right) = right {
for (section_idx, section) in right.sections.iter().enumerate() {
if matches.iter().any(|m| m.right == Some(section_idx)) {
continue;
}
matches.push(SectionMatch {
left: None,
right: Some(section_idx),
section_kind: section.kind,
});
}
}
Ok(matches)
}
fn find_section(obj: Option<&ObjInfo>, name: &str, section_kind: ObjSectionKind) -> Option<usize> {
for (section_idx, section) in obj?.sections.iter().enumerate() {
if section.kind != section_kind {
continue;
}
if section.name == name {
return Some(section_idx);
}
}
None
}

12
objdiff-core/src/lib.rs Normal file
View File

@@ -0,0 +1,12 @@
#[cfg(feature = "any-arch")]
pub mod arch;
#[cfg(feature = "bindings")]
pub mod bindings;
#[cfg(feature = "config")]
pub mod config;
#[cfg(feature = "any-arch")]
pub mod diff;
#[cfg(feature = "any-arch")]
pub mod obj;
#[cfg(feature = "any-arch")]
pub mod util;

190
objdiff-core/src/obj/mod.rs Normal file
View File

@@ -0,0 +1,190 @@
pub mod read;
pub mod split_meta;
use std::{borrow::Cow, collections::BTreeMap, fmt, path::PathBuf};
use filetime::FileTime;
use flagset::{flags, FlagSet};
use object::RelocationFlags;
use split_meta::SplitMeta;
use crate::{arch::ObjArch, util::ReallySigned};
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
pub enum ObjSectionKind {
Code,
Data,
Bss,
}
flags! {
pub enum ObjSymbolFlags: u8 {
Global,
Local,
Weak,
Common,
Hidden,
/// Has extra data associated with the symbol
/// (e.g. exception table entry)
HasExtra,
}
}
#[derive(Debug, Copy, Clone, Default)]
pub struct ObjSymbolFlagSet(pub FlagSet<ObjSymbolFlags>);
#[derive(Debug, Clone)]
pub struct ObjSection {
pub name: String,
pub kind: ObjSectionKind,
pub address: u64,
pub size: u64,
pub data: Vec<u8>,
pub orig_index: usize,
pub symbols: Vec<ObjSymbol>,
pub relocations: Vec<ObjReloc>,
pub virtual_address: Option<u64>,
/// Line number info (.line or .debug_line section)
pub line_info: BTreeMap<u64, u32>,
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum ObjInsArgValue {
Signed(i64),
Unsigned(u64),
Opaque(Cow<'static, str>),
}
impl ObjInsArgValue {
pub fn loose_eq(&self, other: &ObjInsArgValue) -> bool {
match (self, other) {
(ObjInsArgValue::Signed(a), ObjInsArgValue::Signed(b)) => a == b,
(ObjInsArgValue::Unsigned(a), ObjInsArgValue::Unsigned(b)) => a == b,
(ObjInsArgValue::Signed(a), ObjInsArgValue::Unsigned(b))
| (ObjInsArgValue::Unsigned(b), ObjInsArgValue::Signed(a)) => *a as u64 == *b,
(ObjInsArgValue::Opaque(a), ObjInsArgValue::Opaque(b)) => a == b,
_ => false,
}
}
}
impl fmt::Display for ObjInsArgValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ObjInsArgValue::Signed(v) => write!(f, "{:#x}", ReallySigned(*v)),
ObjInsArgValue::Unsigned(v) => write!(f, "{:#x}", v),
ObjInsArgValue::Opaque(v) => write!(f, "{}", v),
}
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum ObjInsArg {
PlainText(Cow<'static, str>),
Arg(ObjInsArgValue),
Reloc,
BranchDest(u64),
}
impl ObjInsArg {
#[inline]
pub fn is_plain_text(&self) -> bool { matches!(self, ObjInsArg::PlainText(_)) }
pub fn loose_eq(&self, other: &ObjInsArg) -> bool {
match (self, other) {
(ObjInsArg::Arg(a), ObjInsArg::Arg(b)) => a.loose_eq(b),
(ObjInsArg::Reloc, ObjInsArg::Reloc) => true,
(ObjInsArg::BranchDest(a), ObjInsArg::BranchDest(b)) => a == b,
_ => false,
}
}
}
#[derive(Debug, Clone)]
pub struct ObjIns {
pub address: u64,
pub size: u8,
pub op: u16,
pub mnemonic: Cow<'static, str>,
pub args: Vec<ObjInsArg>,
pub reloc: Option<ObjReloc>,
pub branch_dest: Option<u64>,
/// Line number
pub line: Option<u32>,
/// Formatted instruction
pub formatted: String,
/// Original (unsimplified) instruction
pub orig: Option<String>,
}
impl ObjIns {
/// Iterate over non-PlainText arguments.
#[inline]
pub fn iter_args(&self) -> impl DoubleEndedIterator<Item = &ObjInsArg> {
self.args.iter().filter(|a| !a.is_plain_text())
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
pub enum ObjSymbolKind {
#[default]
Unknown,
Function,
Object,
Section,
}
#[derive(Debug, Clone)]
pub struct ObjSymbol {
pub name: String,
pub demangled_name: Option<String>,
pub address: u64,
pub section_address: u64,
pub size: u64,
pub size_known: bool,
pub kind: ObjSymbolKind,
pub flags: ObjSymbolFlagSet,
pub orig_section_index: Option<usize>,
/// Original virtual address (from .note.split section)
pub virtual_address: Option<u64>,
/// Original index in object symbol table
pub original_index: Option<usize>,
pub bytes: Vec<u8>,
}
pub struct ObjInfo {
pub arch: Box<dyn ObjArch>,
pub path: Option<PathBuf>,
pub timestamp: Option<FileTime>,
pub sections: Vec<ObjSection>,
/// Common BSS symbols
pub common: Vec<ObjSymbol>,
/// Split object metadata (.note.split section)
pub split_meta: Option<SplitMeta>,
}
#[derive(Debug, Clone)]
pub struct ObjReloc {
pub flags: RelocationFlags,
pub address: u64,
pub target: ObjSymbol,
pub addend: i64,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct SymbolRef {
pub section_idx: usize,
pub symbol_idx: usize,
}
pub const SECTION_COMMON: usize = usize::MAX - 1;
impl ObjInfo {
pub fn section_symbol(&self, symbol_ref: SymbolRef) -> (Option<&ObjSection>, &ObjSymbol) {
if symbol_ref.section_idx == SECTION_COMMON {
let symbol = &self.common[symbol_ref.symbol_idx];
return (None, symbol);
}
let section = &self.sections[symbol_ref.section_idx];
let symbol = &section.symbols[symbol_ref.symbol_idx];
(Some(section), symbol)
}
}

View File

@@ -0,0 +1,756 @@
use std::{
collections::{HashMap, HashSet},
fs,
io::Cursor,
mem::size_of,
path::Path,
};
use anyhow::{anyhow, bail, ensure, Context, Result};
use filetime::FileTime;
use flagset::Flags;
use object::{
endian::LittleEndian as LE,
pe::{ImageAuxSymbolFunctionBeginEnd, ImageLinenumber},
read::coff::{CoffFile, CoffHeader, ImageSymbol},
BinaryFormat, File, Object, ObjectSection, ObjectSymbol, RelocationTarget, Section,
SectionIndex, SectionKind, Symbol, SymbolIndex, SymbolKind, SymbolScope,
};
use crate::{
arch::{new_arch, ObjArch},
diff::DiffObjConfig,
obj::{
split_meta::{SplitMeta, SPLITMETA_SECTION},
ObjInfo, ObjReloc, ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlagSet, ObjSymbolFlags,
ObjSymbolKind,
},
util::{read_u16, read_u32},
};
fn to_obj_section_kind(kind: SectionKind) -> Option<ObjSectionKind> {
match kind {
SectionKind::Text => Some(ObjSectionKind::Code),
SectionKind::Data | SectionKind::ReadOnlyData => Some(ObjSectionKind::Data),
SectionKind::UninitializedData => Some(ObjSectionKind::Bss),
_ => None,
}
}
fn to_obj_symbol(
arch: &dyn ObjArch,
obj_file: &File<'_>,
symbol: &Symbol<'_, '_>,
split_meta: Option<&SplitMeta>,
) -> Result<ObjSymbol> {
let mut name = symbol.name().context("Failed to process symbol name")?;
if name.is_empty() {
log::warn!("Found empty sym: {symbol:?}");
name = "?";
}
let mut flags = ObjSymbolFlagSet(ObjSymbolFlags::none());
if symbol.is_global() {
flags = ObjSymbolFlagSet(flags.0 | ObjSymbolFlags::Global);
}
if symbol.is_local() {
flags = ObjSymbolFlagSet(flags.0 | ObjSymbolFlags::Local);
}
if symbol.is_common() {
flags = ObjSymbolFlagSet(flags.0 | ObjSymbolFlags::Common);
}
if symbol.is_weak() {
flags = ObjSymbolFlagSet(flags.0 | ObjSymbolFlags::Weak);
}
if obj_file.format() == BinaryFormat::Elf && symbol.scope() == SymbolScope::Linkage {
flags = ObjSymbolFlagSet(flags.0 | ObjSymbolFlags::Hidden);
}
#[cfg(feature = "ppc")]
if arch.ppc().and_then(|a| a.extab.as_ref()).is_some_and(|e| e.contains_key(&symbol.index().0))
{
flags = ObjSymbolFlagSet(flags.0 | ObjSymbolFlags::HasExtra);
}
let address = arch.symbol_address(symbol);
let section_address = if let Some(section) =
symbol.section_index().and_then(|idx| obj_file.section_by_index(idx).ok())
{
address - section.address()
} else {
address
};
let demangled_name = arch.demangle(name);
// Find the virtual address for the symbol if available
let virtual_address = split_meta
.and_then(|m| m.virtual_addresses.as_ref())
.and_then(|v| v.get(symbol.index().0).cloned());
let bytes = symbol
.section_index()
.and_then(|idx| obj_file.section_by_index(idx).ok())
.and_then(|section| section.data().ok())
.and_then(|data| {
data.get(section_address as usize..(section_address + symbol.size()) as usize)
})
.unwrap_or(&[]);
let kind = match symbol.kind() {
SymbolKind::Text => ObjSymbolKind::Function,
SymbolKind::Data => ObjSymbolKind::Object,
SymbolKind::Section => ObjSymbolKind::Section,
_ => ObjSymbolKind::Unknown,
};
Ok(ObjSymbol {
name: name.to_string(),
demangled_name,
address,
section_address,
size: symbol.size(),
size_known: symbol.size() != 0,
kind,
flags,
orig_section_index: symbol.section_index().map(|i| i.0),
virtual_address,
original_index: Some(symbol.index().0),
bytes: bytes.to_vec(),
})
}
fn filter_sections(obj_file: &File<'_>, split_meta: Option<&SplitMeta>) -> Result<Vec<ObjSection>> {
let mut result = Vec::<ObjSection>::new();
for section in obj_file.sections() {
if section.size() == 0 {
continue;
}
let Some(kind) = to_obj_section_kind(section.kind()) else {
continue;
};
let name = section.name().context("Failed to process section name")?;
let data = section.uncompressed_data().context("Failed to read section data")?;
// Find the virtual address for the section symbol if available
let section_symbol = obj_file.symbols().find(|s| {
s.kind() == SymbolKind::Section && s.section_index() == Some(section.index())
});
let virtual_address = section_symbol.and_then(|s| {
split_meta
.and_then(|m| m.virtual_addresses.as_ref())
.and_then(|v| v.get(s.index().0).cloned())
});
result.push(ObjSection {
name: name.to_string(),
kind,
address: section.address(),
size: section.size(),
data: data.to_vec(),
orig_index: section.index().0,
symbols: Vec::new(),
relocations: Vec::new(),
virtual_address,
line_info: Default::default(),
});
}
result.sort_by(|a, b| a.name.cmp(&b.name));
Ok(result)
}
fn symbols_by_section(
arch: &dyn ObjArch,
obj_file: &File<'_>,
section: &ObjSection,
section_symbols: &[Symbol<'_, '_>],
split_meta: Option<&SplitMeta>,
name_counts: &mut HashMap<String, u32>,
) -> Result<Vec<ObjSymbol>> {
let mut result = Vec::<ObjSymbol>::new();
for symbol in section_symbols {
if symbol.kind() == SymbolKind::Section {
continue;
}
if symbol.is_local() && section.kind == ObjSectionKind::Code {
// TODO strip local syms in diff?
let name = symbol.name().context("Failed to process symbol name")?;
if symbol.size() == 0 || name.starts_with("lbl_") {
continue;
}
}
result.push(to_obj_symbol(arch, obj_file, symbol, split_meta)?);
}
result.sort_by(|a, b| a.address.cmp(&b.address).then(a.size.cmp(&b.size)));
let mut iter = result.iter_mut().peekable();
while let Some(symbol) = iter.next() {
if symbol.size == 0 {
if let Some(next_symbol) = iter.peek() {
symbol.size = next_symbol.address - symbol.address;
} else {
symbol.size = (section.address + section.size) - symbol.address;
}
// Set symbol kind if we ended up with a non-zero size
if symbol.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() {
// Dummy symbol for empty sections
*name_counts.entry(section.name.clone()).or_insert(0) += 1;
let current_count: u32 = *name_counts.get(&section.name).unwrap();
result.push(ObjSymbol {
name: if current_count > 1 {
format!("[{} ({})]", section.name, current_count)
} else {
format!("[{}]", section.name)
},
demangled_name: None,
address: 0,
section_address: 0,
size: section.size,
size_known: true,
kind: match section.kind {
ObjSectionKind::Code => ObjSymbolKind::Function,
ObjSectionKind::Data | ObjSectionKind::Bss => ObjSymbolKind::Object,
},
flags: Default::default(),
orig_section_index: Some(section.orig_index),
virtual_address: None,
original_index: None,
bytes: Vec::new(),
});
}
Ok(result)
}
fn common_symbols(
arch: &dyn ObjArch,
obj_file: &File<'_>,
split_meta: Option<&SplitMeta>,
) -> Result<Vec<ObjSymbol>> {
obj_file
.symbols()
.filter(Symbol::is_common)
.map(|symbol| to_obj_symbol(arch, obj_file, &symbol, split_meta))
.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(
arch: &dyn ObjArch,
obj_file: &File<'_>,
section: &Section,
section_symbols: &[Symbol<'_, '_>],
address: u64,
split_meta: Option<&SplitMeta>,
) -> Result<ObjSymbol> {
if let Some(symbol) = best_symbol(section_symbols, address) {
return to_obj_symbol(arch, obj_file, symbol, split_meta);
}
// Fallback to section symbol
Ok(ObjSymbol {
name: section.name()?.to_string(),
demangled_name: None,
address: section.address(),
section_address: 0,
size: 0,
size_known: false,
kind: ObjSymbolKind::Section,
flags: Default::default(),
orig_section_index: Some(section.index().0),
virtual_address: None,
original_index: None,
bytes: Vec::new(),
})
}
fn relocations_by_section(
arch: &dyn ObjArch,
obj_file: &File<'_>,
section: &ObjSection,
section_symbols: &[Vec<Symbol<'_, '_>>],
split_meta: Option<&SplitMeta>,
) -> Result<Vec<ObjReloc>> {
let obj_section = obj_file.section_by_index(SectionIndex(section.orig_index))?;
let mut relocations = Vec::<ObjReloc>::new();
for (address, reloc) in obj_section.relocations() {
let symbol = match reloc.target() {
RelocationTarget::Symbol(idx) => {
if idx.0 == u32::MAX as usize {
// ???
continue;
}
let Ok(symbol) = obj_file.symbol_by_index(idx) else {
log::warn!(
"Failed to locate relocation {:#x} target symbol {}",
address,
idx.0
);
continue;
};
symbol
}
RelocationTarget::Absolute => {
log::warn!("Ignoring absolute relocation @ {}:{:#x}", section.name, address);
continue;
}
_ => bail!("Unhandled relocation target: {:?}", reloc.target()),
};
let flags = reloc.flags(); // TODO validate reloc here?
let mut addend = if reloc.has_implicit_addend() {
arch.implcit_addend(obj_file, section, address, &reloc)?
} else {
reloc.addend()
};
let target = match symbol.kind() {
SymbolKind::Text | SymbolKind::Data | SymbolKind::Label | SymbolKind::Unknown => {
to_obj_symbol(arch, obj_file, &symbol, split_meta)?
}
SymbolKind::Section => {
ensure!(addend >= 0, "Negative addend in section reloc: {addend}");
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 => bail!("Unhandled relocation symbol type {kind:?}"),
};
relocations.push(ObjReloc { flags, address, target, addend });
}
Ok(relocations)
}
fn line_info(obj_file: &File<'_>, sections: &mut [ObjSection], obj_data: &[u8]) -> Result<()> {
// DWARF 1.1
if let Some(section) = obj_file.section_by_name(".line") {
let data = section.uncompressed_data()?;
let mut reader = Cursor::new(data.as_ref());
let mut text_sections = obj_file.sections().filter(|s| s.kind() == SectionKind::Text);
while reader.position() < data.len() as u64 {
let text_section_index = text_sections
.next()
.ok_or_else(|| anyhow!("Next text section not found for line info"))?
.index()
.0;
let start = reader.position();
let size = read_u32(obj_file, &mut reader)?;
let base_address = read_u32(obj_file, &mut reader)? as u64;
let Some(out_section) =
sections.iter_mut().find(|s| s.orig_index == text_section_index)
else {
// Skip line info for sections we filtered out
reader.set_position(start + size as u64);
continue;
};
let end = start + size as u64;
while reader.position() < end {
let line_number = read_u32(obj_file, &mut reader)?;
let statement_pos = read_u16(obj_file, &mut reader)?;
if statement_pos != 0xFFFF {
log::warn!("Unhandled statement pos {}", statement_pos);
}
let address_delta = read_u32(obj_file, &mut reader)? as u64;
out_section.line_info.insert(base_address + address_delta, line_number);
log::debug!("Line: {:#x} -> {}", base_address + address_delta, line_number);
}
}
}
// DWARF 2+
#[cfg(feature = "dwarf")]
{
let dwarf_cow = gimli::DwarfSections::load(|id| {
Ok::<_, gimli::Error>(
obj_file
.section_by_name(id.name())
.and_then(|section| section.uncompressed_data().ok())
.unwrap_or(std::borrow::Cow::Borrowed(&[][..])),
)
})?;
let endian = match obj_file.endianness() {
object::Endianness::Little => gimli::RunTimeEndian::Little,
object::Endianness::Big => gimli::RunTimeEndian::Big,
};
let dwarf = dwarf_cow.borrow(|section| gimli::EndianSlice::new(section, endian));
let mut iter = dwarf.units();
if let Some(header) = iter.next()? {
let unit = dwarf.unit(header)?;
if let Some(program) = unit.line_program.clone() {
let mut text_sections =
obj_file.sections().filter(|s| s.kind() == SectionKind::Text);
let section_index = text_sections.next().map(|s| s.index().0);
let mut lines = section_index
.and_then(|index| sections.iter_mut().find(|s| s.orig_index == index))
.map(|s| &mut s.line_info);
let mut rows = program.rows();
while let Some((_header, row)) = rows.next_row()? {
if let (Some(line), Some(lines)) = (row.line(), &mut lines) {
lines.insert(row.address(), line.get() as u32);
}
if row.end_sequence() {
// The next row is the start of a new sequence, which means we must
// advance to the next .text section.
let section_index = text_sections.next().map(|s| s.index().0);
lines = section_index
.and_then(|index| sections.iter_mut().find(|s| s.orig_index == index))
.map(|s| &mut s.line_info);
}
}
}
}
if iter.next()?.is_some() {
log::warn!("Multiple units found in DWARF data, only processing the first");
}
}
// COFF
if let File::Coff(coff) = obj_file {
line_info_coff(coff, sections, obj_data)?;
}
Ok(())
}
fn line_info_coff(coff: &CoffFile, sections: &mut [ObjSection], obj_data: &[u8]) -> Result<()> {
let symbol_table = coff.coff_header().symbols(obj_data)?;
// Enumerate over all sections.
for sect in coff.sections() {
let ptr_linenums = sect.coff_section().pointer_to_linenumbers.get(LE) as usize;
let num_linenums = sect.coff_section().number_of_linenumbers.get(LE) as usize;
// If we have no line number, skip this section.
if num_linenums == 0 {
continue;
}
// Find this section in our out_section. If it's not in out_section,
// skip it.
let Some(out_section) = sections.iter_mut().find(|s| s.orig_index == sect.index().0) else {
continue;
};
// Turn the line numbers into an ImageLinenumber slice.
let Some(linenums) =
&obj_data.get(ptr_linenums..ptr_linenums + num_linenums * size_of::<ImageLinenumber>())
else {
continue;
};
let Ok(linenums) = object::pod::slice_from_all_bytes::<ImageLinenumber>(linenums) else {
continue;
};
// In COFF, the line numbers are stored relative to the start of the
// function. Because of this, we need to know the line number where the
// function starts, so we can sum the two and get the line number
// relative to the start of the file.
//
// This variable stores the line number where the function currently
// being processed starts. It is set to None when we failed to find the
// line number of the start of the function.
let mut cur_fun_start_linenumber = None;
for linenum in linenums {
let line_number = linenum.linenumber.get(LE);
if line_number == 0 {
// Starting a new function. We need to find the line where that
// function is located in the file. To do this, we need to find
// the `.bf` symbol "associated" with this function. The .bf
// symbol will have a Function Begin/End Auxillary Record, which
// contains the line number of the start of the function.
// First, set cur_fun_start_linenumber to None. If we fail to
// find the start of the function, this will make sure the
// subsequent line numbers will be ignored until the next start
// of function.
cur_fun_start_linenumber = None;
// Get the symbol associated with this function. We'll need it
// for logging purposes, but also to acquire its Function
// Auxillary Record, which tells us where to find our .bf symbol.
let symtable_entry = linenum.symbol_table_index_or_virtual_address.get(LE);
let Ok(symbol) = symbol_table.symbol(SymbolIndex(symtable_entry as usize)) else {
continue;
};
let Ok(aux_fun) = symbol_table.aux_function(SymbolIndex(symtable_entry as usize))
else {
continue;
};
// Get the .bf symbol associated with this symbol. To do so, we
// look at the Function Auxillary Record's tag_index, which is
// an index in the symbol table pointing to our .bf symbol.
if aux_fun.tag_index.get(LE) == 0 {
continue;
}
let Ok(bf_symbol) =
symbol_table.symbol(SymbolIndex(aux_fun.tag_index.get(LE) as usize))
else {
continue;
};
// Do some sanity checks that we are, indeed, looking at a .bf
// symbol.
if bf_symbol.name(symbol_table.strings()) != Ok(b".bf") {
continue;
}
// Get the Function Begin/End Auxillary Record associated with
// our .bf symbol, where we'll fine the linenumber of the start
// of our function.
let Ok(bf_aux) = symbol_table.get::<ImageAuxSymbolFunctionBeginEnd>(
SymbolIndex(aux_fun.tag_index.get(LE) as usize),
1,
) else {
continue;
};
// Set cur_fun_start_linenumber so the following linenumber
// records will know at what line the current function start.
cur_fun_start_linenumber = Some(bf_aux.linenumber.get(LE) as u32);
// Let's also synthesize a line number record from the start of
// the function, as the linenumber records don't always cover it.
out_section.line_info.insert(
sect.address() + symbol.value() as u64,
bf_aux.linenumber.get(LE) as u32,
);
} else if let Some(cur_linenumber) = cur_fun_start_linenumber {
let vaddr = linenum.symbol_table_index_or_virtual_address.get(LE);
out_section
.line_info
.insert(sect.address() + vaddr as u64, cur_linenumber + line_number as u32);
}
}
}
Ok(())
}
fn update_combined_symbol(symbol: ObjSymbol, address_change: i64) -> Result<ObjSymbol> {
Ok(ObjSymbol {
name: symbol.name,
demangled_name: symbol.demangled_name,
address: (symbol.address as i64 + address_change).try_into()?,
section_address: (symbol.section_address as i64 + address_change).try_into()?,
size: symbol.size,
size_known: symbol.size_known,
kind: symbol.kind,
flags: symbol.flags,
orig_section_index: symbol.orig_section_index,
virtual_address: if let Some(virtual_address) = symbol.virtual_address {
Some((virtual_address as i64 + address_change).try_into()?)
} else {
None
},
original_index: symbol.original_index,
bytes: symbol.bytes,
})
}
fn combine_sections(section: ObjSection, combine: ObjSection) -> Result<ObjSection> {
let mut data = section.data;
data.extend(combine.data);
let address_change: i64 = (section.address + section.size) as i64 - combine.address as i64;
let mut symbols = section.symbols;
for symbol in combine.symbols {
symbols.push(update_combined_symbol(symbol, address_change)?);
}
let mut relocations = section.relocations;
for reloc in combine.relocations {
relocations.push(ObjReloc {
flags: reloc.flags,
address: (reloc.address as i64 + address_change).try_into()?,
target: reloc.target, // TODO: Should be updated?
addend: reloc.addend,
});
}
let mut line_info = section.line_info;
for (addr, line) in combine.line_info {
let key = (addr as i64 + address_change).try_into()?;
line_info.insert(key, line);
}
Ok(ObjSection {
name: section.name,
kind: section.kind,
address: section.address,
size: section.size + combine.size,
data,
orig_index: section.orig_index,
symbols,
relocations,
virtual_address: section.virtual_address,
line_info,
})
}
fn combine_data_sections(sections: &mut Vec<ObjSection>) -> Result<()> {
let names_to_combine: HashSet<_> = sections
.iter()
.filter(|s| s.kind == ObjSectionKind::Data)
.map(|s| s.name.clone())
.collect();
for name in names_to_combine {
// Take section with lowest index
let (mut section_index, _) = sections
.iter()
.enumerate()
.filter(|(_, s)| s.name == name)
.min_by_key(|(_, s)| s.orig_index)
// Should not happen
.context("No combine section found with name")?;
let mut section = sections.remove(section_index);
// Remove equally named sections
let mut combines = vec![];
for i in (0..sections.len()).rev() {
if sections[i].name != name || sections[i].orig_index == section.orig_index {
continue;
}
combines.push(sections.remove(i));
if i < section_index {
section_index -= 1;
}
}
// Combine sections ordered by index
combines.sort_unstable_by_key(|c| c.orig_index);
for combine in combines {
section = combine_sections(section, combine)?;
}
sections.insert(section_index, section);
}
Ok(())
}
pub fn read(obj_path: &Path, config: &DiffObjConfig) -> Result<ObjInfo> {
let (data, timestamp) = {
let file = fs::File::open(obj_path)?;
let timestamp = FileTime::from_last_modification_time(&file.metadata()?);
(unsafe { memmap2::Mmap::map(&file) }?, timestamp)
};
let mut obj = parse(&data, config)?;
obj.path = Some(obj_path.to_owned());
obj.timestamp = Some(timestamp);
Ok(obj)
}
pub fn parse(data: &[u8], config: &DiffObjConfig) -> Result<ObjInfo> {
let obj_file = File::parse(data)?;
let arch = new_arch(&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 section_name_counts: HashMap<String, u32> = HashMap::new();
for section in &mut sections {
section.symbols = symbols_by_section(
arch.as_ref(),
&obj_file,
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(),
)?;
}
if config.combine_data_sections {
combine_data_sections(&mut sections)?;
}
line_info(&obj_file, &mut sections, data)?;
let common = common_symbols(arch.as_ref(), &obj_file, split_meta.as_ref())?;
Ok(ObjInfo { arch, path: None, timestamp: None, sections, common, split_meta })
}
pub fn has_function(obj_path: &Path, symbol_name: &str) -> Result<bool> {
let data = {
let file = fs::File::open(obj_path)?;
unsafe { memmap2::Mmap::map(&file) }?
};
Ok(File::parse(&*data)?
.symbol_by_name(symbol_name)
.filter(|o| o.kind() == SymbolKind::Text)
.is_some())
}
fn split_meta(obj_file: &File<'_>) -> Result<Option<SplitMeta>> {
Ok(if let Some(section) = obj_file.section_by_name(SPLITMETA_SECTION) {
Some(SplitMeta::from_section(section, obj_file.endianness(), obj_file.is_64())?)
} else {
None
})
}

View File

@@ -0,0 +1,223 @@
use std::{io, io::Write};
use object::{elf::SHT_NOTE, Endian, ObjectSection};
pub const SPLITMETA_SECTION: &str = ".note.split";
pub const SHT_SPLITMETA: u32 = SHT_NOTE;
pub const ELF_NOTE_SPLIT: &[u8] = b"Split";
/// This is used to store metadata about the source of an object file,
/// such as the original virtual addresses and the tool that wrote it.
#[derive(Debug, Default, Clone)]
pub struct SplitMeta {
/// The tool that generated the object. Informational only.
pub generator: Option<String>,
/// The name of the source module. (e.g. the DOL or REL name)
pub module_name: Option<String>,
/// The ID of the source module. (e.g. the DOL or REL ID)
pub module_id: Option<u32>,
/// Original virtual addresses of each symbol in the object.
/// Index 0 is the ELF null symbol.
pub virtual_addresses: Option<Vec<u64>>,
}
const NT_SPLIT_GENERATOR: u32 = u32::from_be_bytes(*b"GENR");
const NT_SPLIT_MODULE_NAME: u32 = u32::from_be_bytes(*b"MODN");
const NT_SPLIT_MODULE_ID: u32 = u32::from_be_bytes(*b"MODI");
const NT_SPLIT_VIRTUAL_ADDRESSES: u32 = u32::from_be_bytes(*b"VIRT");
impl SplitMeta {
pub fn from_section<E>(section: object::Section, e: E, is_64: bool) -> io::Result<Self>
where E: Endian {
let mut result = SplitMeta::default();
let data = section.uncompressed_data().map_err(object_io_error)?;
let mut iter = NoteIterator::new(data.as_ref(), section.align(), e, is_64)?;
while let Some(note) = iter.next(e)? {
if note.name != ELF_NOTE_SPLIT {
continue;
}
match note.n_type {
NT_SPLIT_GENERATOR => {
let string = String::from_utf8(note.desc.to_vec())
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
result.generator = Some(string);
}
NT_SPLIT_MODULE_NAME => {
let string = String::from_utf8(note.desc.to_vec())
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
result.module_name = Some(string);
}
NT_SPLIT_MODULE_ID => {
result.module_id =
Some(e.read_u32_bytes(note.desc.try_into().map_err(|_| {
io::Error::new(io::ErrorKind::InvalidData, "Invalid module ID size")
})?));
}
NT_SPLIT_VIRTUAL_ADDRESSES => {
let vec = if is_64 {
let mut vec = vec![0u64; note.desc.len() / 8];
for (i, v) in vec.iter_mut().enumerate() {
*v =
e.read_u64_bytes(note.desc[i * 8..(i + 1) * 8].try_into().unwrap());
}
vec
} else {
let mut vec = vec![0u64; note.desc.len() / 4];
for (i, v) in vec.iter_mut().enumerate() {
*v = e.read_u32_bytes(note.desc[i * 4..(i + 1) * 4].try_into().unwrap())
as u64;
}
vec
};
result.virtual_addresses = Some(vec);
}
_ => {
// Ignore unknown sections
}
}
}
Ok(result)
}
pub fn to_writer<E, W>(&self, writer: &mut W, e: E, is_64: bool) -> io::Result<()>
where
E: Endian,
W: Write + ?Sized,
{
if let Some(generator) = &self.generator {
write_note_header(writer, e, NT_SPLIT_GENERATOR, generator.len())?;
writer.write_all(generator.as_bytes())?;
align_data_to_4(writer, generator.len())?;
}
if let Some(module_name) = &self.module_name {
write_note_header(writer, e, NT_SPLIT_MODULE_NAME, module_name.len())?;
writer.write_all(module_name.as_bytes())?;
align_data_to_4(writer, module_name.len())?;
}
if let Some(module_id) = self.module_id {
write_note_header(writer, e, NT_SPLIT_MODULE_ID, 4)?;
writer.write_all(&e.write_u32_bytes(module_id))?;
}
if let Some(virtual_addresses) = &self.virtual_addresses {
let count = virtual_addresses.len();
let size = if is_64 { count * 8 } else { count * 4 };
write_note_header(writer, e, NT_SPLIT_VIRTUAL_ADDRESSES, size)?;
if is_64 {
for &addr in virtual_addresses {
writer.write_all(&e.write_u64_bytes(addr))?;
}
} else {
for &addr in virtual_addresses {
writer.write_all(&e.write_u32_bytes(addr as u32))?;
}
}
}
Ok(())
}
pub fn write_size(&self, is_64: bool) -> usize {
let mut size = 0;
if let Some(generator) = self.generator.as_deref() {
size += NOTE_HEADER_SIZE + generator.len();
size = align_size_to_4(size);
}
if let Some(module_name) = self.module_name.as_deref() {
size += NOTE_HEADER_SIZE + module_name.len();
size = align_size_to_4(size);
}
if self.module_id.is_some() {
size += NOTE_HEADER_SIZE + 4;
size = align_size_to_4(size);
}
if let Some(virtual_addresses) = self.virtual_addresses.as_deref() {
size += NOTE_HEADER_SIZE + if is_64 { 8 } else { 4 } * virtual_addresses.len();
size = align_size_to_4(size);
}
size
}
}
/// Convert an object::read::Error to an io::Error.
fn object_io_error(err: object::read::Error) -> io::Error {
io::Error::new(io::ErrorKind::InvalidData, err)
}
/// An ELF note entry.
struct Note<'data> {
n_type: u32,
name: &'data [u8],
desc: &'data [u8],
}
/// object::read::elf::NoteIterator is awkward to use generically,
/// so wrap it in our own iterator.
enum NoteIterator<'data, E>
where E: Endian
{
B32(object::read::elf::NoteIterator<'data, object::elf::FileHeader32<E>>),
B64(object::read::elf::NoteIterator<'data, object::elf::FileHeader64<E>>),
}
impl<'data, E> NoteIterator<'data, E>
where E: Endian
{
fn new(data: &'data [u8], align: u64, e: E, is_64: bool) -> io::Result<Self> {
Ok(if is_64 {
NoteIterator::B64(
object::read::elf::NoteIterator::new(e, align, data).map_err(object_io_error)?,
)
} else {
NoteIterator::B32(
object::read::elf::NoteIterator::new(e, align as u32, data)
.map_err(object_io_error)?,
)
})
}
fn next(&mut self, e: E) -> io::Result<Option<Note<'data>>> {
match self {
NoteIterator::B32(iter) => Ok(iter.next().map_err(object_io_error)?.map(|note| Note {
n_type: note.n_type(e),
name: note.name(),
desc: note.desc(),
})),
NoteIterator::B64(iter) => Ok(iter.next().map_err(object_io_error)?.map(|note| Note {
n_type: note.n_type(e),
name: note.name(),
desc: note.desc(),
})),
}
}
}
fn align_size_to_4(size: usize) -> usize { (size + 3) & !3 }
fn align_data_to_4<W: Write + ?Sized>(writer: &mut W, len: usize) -> io::Result<()> {
const ALIGN_BYTES: &[u8] = &[0; 4];
if len % 4 != 0 {
writer.write_all(&ALIGN_BYTES[..4 - len % 4])?;
}
Ok(())
}
// ELF note format:
// Name Size | 4 bytes (integer)
// Desc Size | 4 bytes (integer)
// Type | 4 bytes (usually interpreted as an integer)
// Name | variable size, padded to a 4 byte boundary
// Desc | variable size, padded to a 4 byte boundary
const NOTE_HEADER_SIZE: usize = 12 + ((ELF_NOTE_SPLIT.len() + 4) & !3);
fn write_note_header<E, W>(writer: &mut W, e: E, kind: u32, desc_len: usize) -> io::Result<()>
where
E: Endian,
W: Write + ?Sized,
{
writer.write_all(&e.write_u32_bytes(ELF_NOTE_SPLIT.len() as u32 + 1))?; // Name Size
writer.write_all(&e.write_u32_bytes(desc_len as u32))?; // Desc Size
writer.write_all(&e.write_u32_bytes(kind))?; // Type
writer.write_all(ELF_NOTE_SPLIT)?; // Name
writer.write_all(&[0; 1])?; // Null terminator
align_data_to_4(writer, ELF_NOTE_SPLIT.len() + 1)?;
Ok(())
}

38
objdiff-core/src/util.rs Normal file
View File

@@ -0,0 +1,38 @@
use std::{
fmt::{LowerHex, UpperHex},
io::Read,
};
use anyhow::Result;
use byteorder::{NativeEndian, ReadBytesExt};
use num_traits::PrimInt;
use object::{Endian, Object};
// https://stackoverflow.com/questions/44711012/how-do-i-format-a-signed-integer-to-a-sign-aware-hexadecimal-representation
pub struct ReallySigned<N: PrimInt>(pub(crate) N);
impl<N: PrimInt> LowerHex for ReallySigned<N> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let num = self.0.to_i64().unwrap();
let prefix = if f.alternate() { "0x" } else { "" };
let bare_hex = format!("{:x}", num.abs());
f.pad_integral(num >= 0, prefix, &bare_hex)
}
}
impl<N: PrimInt> UpperHex for ReallySigned<N> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let num = self.0.to_i64().unwrap();
let prefix = if f.alternate() { "0x" } else { "" };
let bare_hex = format!("{:X}", num.abs());
f.pad_integral(num >= 0, prefix, &bare_hex)
}
}
pub fn read_u32<R: Read>(obj_file: &object::File, reader: &mut R) -> Result<u32> {
Ok(obj_file.endianness().read_u32(reader.read_u32::<NativeEndian>()?))
}
pub fn read_u16<R: Read>(obj_file: &object::File, reader: &mut R) -> Result<u16> {
Ok(obj_file.endianness().read_u16(reader.read_u16::<NativeEndian>()?))
}

109
objdiff-gui/Cargo.toml Normal file
View File

@@ -0,0 +1,109 @@
[package]
name = "objdiff-gui"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
readme = "../README.md"
description = """
A local diffing tool for decompilation projects.
"""
publish = false
build = "build.rs"
[[bin]]
name = "objdiff"
path = "src/main.rs"
[features]
default = ["glow", "wgpu", "wsl"]
glow = ["eframe/glow"]
wgpu = ["eframe/wgpu", "dep:wgpu"]
wsl = []
[dependencies]
anyhow = "1.0"
bytes = "1.9"
cfg-if = "1.0"
const_format = "0.2"
cwdemangle = "1.0"
cwextab = "1.0.2"
dirs = "5.0"
egui = "0.29"
egui_extras = "0.29"
filetime = "0.2"
float-ord = "0.3"
font-kit = "0.14"
globset = { version = "0.4", features = ["serde1"] }
log = "0.4"
notify = { git = "https://github.com/notify-rs/notify", rev = "128bf6230c03d39dbb7f301ff7b20e594e34c3a2" }
objdiff-core = { path = "../objdiff-core", features = ["all"] }
open = "5.3"
png = "0.17"
pollster = "0.4"
regex = "1.11"
rfd = { version = "0.15" } #, default-features = false, features = ['xdg-portal']
rlwinmdec = "1.0"
ron = "0.8"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
shell-escape = "0.1"
strum = { version = "0.26", features = ["derive"] }
tempfile = "3.14"
time = { version = "0.3", features = ["formatting", "local-offset"] }
# Keep version in sync with egui
[dependencies.eframe]
version = "0.29"
features = [
"default_fonts",
"persistence",
"wayland",
"x11",
]
default-features = false
# Keep version in sync with eframe
[dependencies.wgpu]
version = "22.1"
features = [
"dx12",
"metal",
"webgpu",
]
optional = true
default-features = false
# For Linux static binaries, use rustls
[target.'cfg(target_os = "linux")'.dependencies]
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "multipart", "rustls-tls"] }
self_update = { version = "0.41", default-features = false, features = ["rustls"] }
# For all other platforms, use native TLS
[target.'cfg(not(target_os = "linux"))'.dependencies]
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "multipart", "default-tls"] }
self_update = "0.41"
[target.'cfg(windows)'.dependencies]
path-slash = "0.2"
winapi = "0.3"
[target.'cfg(unix)'.dependencies]
exec = "0.3"
# native:
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
# web:
[target.'cfg(target_arch = "wasm32")'.dependencies]
console_error_panic_hook = "0.1"
tracing-wasm = "0.2"
[build-dependencies]
anyhow = "1.0"
[target.'cfg(windows)'.build-dependencies]
tauri-winres = "0.1"

BIN
objdiff-gui/assets/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
objdiff-gui/assets/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

12
objdiff-gui/build.rs Normal file
View File

@@ -0,0 +1,12 @@
use anyhow::Result;
fn main() -> Result<()> {
#[cfg(windows)]
{
let mut res = tauri_winres::WindowsResource::new();
res.set_icon("assets/icon.ico");
res.set_language(0x0409); // US English
res.compile()?;
}
Ok(())
}

898
objdiff-gui/src/app.rs Normal file
View File

@@ -0,0 +1,898 @@
use std::{
default::Default,
fs,
path::{Path, PathBuf},
rc::Rc,
sync::{
atomic::{AtomicBool, Ordering},
Arc, Mutex, RwLock,
},
time::Instant,
};
use filetime::FileTime;
use globset::{Glob, GlobSet};
use notify::{RecursiveMode, Watcher};
use objdiff_core::{
config::{
build_globset, save_project_config, ProjectConfig, ProjectConfigInfo, ProjectObject,
ScratchConfig, SymbolMappings, DEFAULT_WATCH_PATTERNS,
},
diff::DiffObjConfig,
};
use time::UtcOffset;
use crate::{
app_config::{deserialize_config, AppConfigVersion},
config::{load_project_config, ProjectObjectNode},
jobs::{
objdiff::{start_build, ObjDiffConfig},
Job, JobQueue, JobResult, JobStatus,
},
views::{
appearance::{appearance_window, Appearance},
config::{
arch_config_window, config_ui, project_window, ConfigViewState, CONFIG_DISABLED_TEXT,
},
data_diff::data_diff_ui,
debug::debug_window,
demangle::{demangle_window, DemangleViewState},
extab_diff::extab_diff_ui,
frame_history::FrameHistory,
function_diff::function_diff_ui,
graphics::{graphics_window, GraphicsConfig, GraphicsViewState},
jobs::{jobs_menu_ui, jobs_window},
rlwinm::{rlwinm_decode_window, RlwinmDecodeViewState},
symbol_diff::{symbol_diff_ui, DiffViewAction, DiffViewNavigation, DiffViewState, View},
},
};
pub struct ViewState {
pub jobs: JobQueue,
pub config_state: ConfigViewState,
pub demangle_state: DemangleViewState,
pub rlwinm_decode_state: RlwinmDecodeViewState,
pub diff_state: DiffViewState,
pub graphics_state: GraphicsViewState,
pub frame_history: FrameHistory,
pub show_appearance_config: bool,
pub show_demangle: bool,
pub show_rlwinm_decode: bool,
pub show_project_config: bool,
pub show_arch_config: bool,
pub show_debug: bool,
pub show_graphics: bool,
pub show_jobs: bool,
pub show_side_panel: bool,
}
impl Default for ViewState {
fn default() -> Self {
Self {
jobs: Default::default(),
config_state: Default::default(),
demangle_state: Default::default(),
rlwinm_decode_state: Default::default(),
diff_state: Default::default(),
graphics_state: Default::default(),
frame_history: Default::default(),
show_appearance_config: false,
show_demangle: false,
show_rlwinm_decode: false,
show_project_config: false,
show_arch_config: false,
show_debug: false,
show_graphics: false,
show_jobs: false,
show_side_panel: true,
}
}
}
/// The configuration for a single object file.
#[derive(Default, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct ObjectConfig {
pub name: String,
pub target_path: Option<PathBuf>,
pub base_path: Option<PathBuf>,
pub reverse_fn_order: Option<bool>,
pub complete: Option<bool>,
pub scratch: Option<ScratchConfig>,
pub source_path: Option<String>,
#[serde(default)]
pub symbol_mappings: SymbolMappings,
}
impl From<&ProjectObject> for ObjectConfig {
fn from(object: &ProjectObject) -> Self {
Self {
name: object.name().to_string(),
target_path: object.target_path.clone(),
base_path: object.base_path.clone(),
reverse_fn_order: object.reverse_fn_order(),
complete: object.complete(),
scratch: object.scratch.clone(),
source_path: object.source_path().cloned(),
symbol_mappings: object.symbol_mappings.clone().unwrap_or_default(),
}
}
}
#[inline]
fn bool_true() -> bool { true }
#[inline]
fn default_watch_patterns() -> Vec<Glob> {
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect()
}
pub struct AppState {
pub config: AppConfig,
pub objects: Vec<ProjectObject>,
pub object_nodes: Vec<ProjectObjectNode>,
pub watcher_change: bool,
pub config_change: bool,
pub obj_change: bool,
pub queue_build: bool,
pub queue_reload: bool,
pub current_project_config: Option<ProjectConfig>,
pub project_config_info: Option<ProjectConfigInfo>,
pub last_mod_check: Instant,
/// The right object symbol name that we're selecting a left symbol for
pub selecting_left: Option<String>,
/// The left object symbol name that we're selecting a right symbol for
pub selecting_right: Option<String>,
pub config_error: Option<String>,
}
impl Default for AppState {
fn default() -> Self {
Self {
config: Default::default(),
objects: vec![],
object_nodes: vec![],
watcher_change: false,
config_change: false,
obj_change: false,
queue_build: false,
queue_reload: false,
current_project_config: None,
project_config_info: None,
last_mod_check: Instant::now(),
selecting_left: None,
selecting_right: None,
config_error: None,
}
}
}
#[derive(Clone, serde::Deserialize, serde::Serialize)]
pub struct AppConfig {
// TODO: https://github.com/ron-rs/ron/pull/455
// #[serde(flatten)]
// pub version: AppConfigVersion,
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<ObjectConfig>,
#[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 = "default_watch_patterns")]
pub watch_patterns: Vec<Glob>,
#[serde(default)]
pub recent_projects: Vec<PathBuf>,
#[serde(default)]
pub diff_obj_config: DiffObjConfig,
}
impl Default for AppConfig {
fn default() -> Self {
Self {
version: AppConfigVersion::default().version,
custom_make: None,
custom_args: None,
selected_wsl_distro: None,
project_dir: None,
target_obj_dir: None,
base_obj_dir: None,
selected_obj: None,
build_base: true,
build_target: false,
rebuild_on_changes: true,
auto_update_check: true,
watch_patterns: DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect(),
recent_projects: vec![],
diff_obj_config: Default::default(),
}
}
}
impl AppState {
pub fn set_project_dir(&mut self, path: PathBuf) {
self.config.recent_projects.retain(|p| p != &path);
if self.config.recent_projects.len() > 9 {
self.config.recent_projects.truncate(9);
}
self.config.recent_projects.insert(0, path.clone());
self.config.project_dir = Some(path);
self.config.target_obj_dir = None;
self.config.base_obj_dir = None;
self.config.selected_obj = None;
self.config.build_target = false;
self.objects.clear();
self.object_nodes.clear();
self.watcher_change = true;
self.config_change = true;
self.obj_change = true;
self.queue_build = false;
self.current_project_config = None;
self.project_config_info = None;
self.selecting_left = None;
self.selecting_right = None;
}
pub fn set_target_obj_dir(&mut self, path: PathBuf) {
self.config.target_obj_dir = Some(path);
self.config.selected_obj = None;
self.obj_change = true;
self.queue_build = false;
self.selecting_left = None;
self.selecting_right = None;
}
pub fn set_base_obj_dir(&mut self, path: PathBuf) {
self.config.base_obj_dir = Some(path);
self.config.selected_obj = None;
self.obj_change = true;
self.queue_build = false;
self.selecting_left = None;
self.selecting_right = None;
}
pub fn set_selected_obj(&mut self, config: ObjectConfig) {
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.queue_build = false;
self.selecting_left = None;
self.selecting_right = None;
}
pub fn set_selecting_left(&mut self, right: &str) {
let Some(object) = self.config.selected_obj.as_mut() else {
return;
};
object.symbol_mappings.remove_by_right(right);
self.selecting_left = Some(right.to_string());
self.queue_reload = true;
self.save_config();
}
pub fn set_selecting_right(&mut self, left: &str) {
let Some(object) = self.config.selected_obj.as_mut() else {
return;
};
object.symbol_mappings.remove_by_left(left);
self.selecting_right = Some(left.to_string());
self.queue_reload = true;
self.save_config();
}
pub fn set_symbol_mapping(&mut self, left: String, right: String) {
let Some(object) = self.config.selected_obj.as_mut() else {
log::warn!("No selected object");
return;
};
self.selecting_left = None;
self.selecting_right = None;
if left == right {
object.symbol_mappings.remove_by_left(&left);
object.symbol_mappings.remove_by_right(&right);
} else {
object.symbol_mappings.insert(left.clone(), right.clone());
}
self.queue_reload = true;
self.save_config();
}
pub fn clear_selection(&mut self) {
self.selecting_left = None;
self.selecting_right = None;
self.queue_reload = true;
}
pub fn clear_mappings(&mut self) {
self.selecting_left = None;
self.selecting_right = None;
if let Some(object) = self.config.selected_obj.as_mut() {
object.symbol_mappings.clear();
}
self.queue_reload = true;
self.save_config();
}
pub fn is_selecting_symbol(&self) -> bool {
self.selecting_left.is_some() || self.selecting_right.is_some()
}
pub fn save_config(&mut self) {
let (Some(config), Some(info)) =
(self.current_project_config.as_mut(), self.project_config_info.as_mut())
else {
return;
};
// Update the project config with the current state
if let Some(object) = self.config.selected_obj.as_ref() {
if let Some(existing) = config.units.as_mut().and_then(|v| {
v.iter_mut().find(|u| u.name.as_ref().is_some_and(|n| n == &object.name))
}) {
existing.symbol_mappings = if object.symbol_mappings.is_empty() {
None
} else {
Some(object.symbol_mappings.clone())
};
}
if let Some(existing) =
self.objects.iter_mut().find(|u| u.name.as_ref().is_some_and(|n| n == &object.name))
{
existing.symbol_mappings = if object.symbol_mappings.is_empty() {
None
} else {
Some(object.symbol_mappings.clone())
};
}
}
// Save the updated project config
match save_project_config(config, info) {
Ok(new_info) => *info = new_info,
Err(e) => {
log::error!("Failed to save project config: {e}");
self.config_error = Some(format!("Failed to save project config: {e}"));
}
}
}
}
pub type AppStateRef = Arc<RwLock<AppState>>;
#[derive(Default)]
pub struct App {
appearance: Appearance,
view_state: ViewState,
state: AppStateRef,
modified: Arc<AtomicBool>,
watcher: Option<notify::RecommendedWatcher>,
app_path: Option<PathBuf>,
relaunch_path: Rc<Mutex<Option<PathBuf>>>,
should_relaunch: bool,
}
pub const APPEARANCE_KEY: &str = "appearance";
pub const CONFIG_KEY: &str = "app_config";
impl App {
/// Called once before the first frame.
pub fn new(
cc: &eframe::CreationContext<'_>,
utc_offset: UtcOffset,
relaunch_path: Rc<Mutex<Option<PathBuf>>>,
app_path: Option<PathBuf>,
graphics_config: GraphicsConfig,
graphics_config_path: Option<PathBuf>,
) -> Self {
// Load previous app state (if any).
// Note that you must enable the `persistence` feature for this to work.
let mut app = Self::default();
if let Some(storage) = cc.storage {
if let Some(appearance) = eframe::get_value::<Appearance>(storage, APPEARANCE_KEY) {
app.appearance = appearance;
}
if let Some(config) = deserialize_config(storage) {
let mut state = AppState { config, ..Default::default() };
if state.config.project_dir.is_some() {
state.config_change = true;
state.watcher_change = true;
}
if state.config.selected_obj.is_some() {
state.queue_build = true;
}
app.view_state.config_state.queue_check_update = state.config.auto_update_check;
app.state = Arc::new(RwLock::new(state));
}
}
app.appearance.init_fonts(&cc.egui_ctx);
app.appearance.utc_offset = utc_offset;
app.app_path = app_path;
app.relaunch_path = relaunch_path;
#[cfg(feature = "wgpu")]
if let Some(wgpu_render_state) = &cc.wgpu_render_state {
use eframe::egui_wgpu::wgpu::Backend;
let info = wgpu_render_state.adapter.get_info();
app.view_state.graphics_state.active_backend = match info.backend {
Backend::Empty => "Unknown",
Backend::Vulkan => "Vulkan",
Backend::Metal => "Metal",
Backend::Dx12 => "DirectX 12",
Backend::Gl => "OpenGL",
Backend::BrowserWebGpu => "WebGPU",
}
.to_string();
app.view_state.graphics_state.active_device.clone_from(&info.name);
}
#[cfg(feature = "glow")]
if let Some(gl) = &cc.gl {
use eframe::glow::HasContext;
app.view_state.graphics_state.active_backend = "OpenGL (Fallback)".to_string();
app.view_state.graphics_state.active_device =
unsafe { gl.get_parameter_string(0x1F01) }; // GL_RENDERER
}
app.view_state.graphics_state.graphics_config = graphics_config;
app.view_state.graphics_state.graphics_config_path = graphics_config_path;
app
}
fn pre_update(&mut self, ctx: &egui::Context) {
self.appearance.pre_update(ctx);
let ViewState { jobs, diff_state, config_state, .. } = &mut self.view_state;
let mut results = vec![];
for (job, result) in jobs.iter_finished() {
match result {
Ok(result) => {
log::info!("Job {} finished", job.id);
match result {
JobResult::None => {
if let Some(err) = &job.context.status.read().unwrap().error {
log::error!("{:?}", err);
}
}
JobResult::Update(state) => {
if let Ok(mut guard) = self.relaunch_path.lock() {
*guard = Some(state.exe_path);
self.should_relaunch = true;
}
}
_ => results.push(result),
}
}
Err(err) => {
let err = if let Some(msg) = err.downcast_ref::<&'static str>() {
anyhow::Error::msg(*msg)
} else if let Some(msg) = err.downcast_ref::<String>() {
anyhow::Error::msg(msg.clone())
} else {
anyhow::Error::msg("Thread panicked")
};
let result = job.context.status.write();
if let Ok(mut guard) = result {
guard.error = Some(err);
} else {
drop(result);
job.context.status = Arc::new(RwLock::new(JobStatus {
title: "Error".to_string(),
progress_percent: 0.0,
progress_items: None,
status: String::new(),
error: Some(err),
}));
}
}
}
}
jobs.results.append(&mut results);
jobs.clear_finished();
diff_state.pre_update(jobs, &self.state);
config_state.pre_update(jobs, &self.state);
debug_assert!(jobs.results.is_empty());
}
fn post_update(&mut self, ctx: &egui::Context, action: Option<DiffViewAction>) {
self.appearance.post_update(ctx);
let ViewState { jobs, diff_state, config_state, graphics_state, .. } = &mut self.view_state;
config_state.post_update(ctx, jobs, &self.state);
diff_state.post_update(action, ctx, jobs, &self.state);
let Ok(mut state) = self.state.write() else {
return;
};
let state = &mut *state;
let mut mod_check = false;
if state.last_mod_check.elapsed().as_millis() >= 500 {
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 {
state.config_change = false;
match load_project_config(state) {
Ok(()) => state.config_error = None,
Err(e) => {
log::error!("Failed to load project config: {e}");
state.config_error = Some(format!("{e}"));
}
}
}
if state.watcher_change {
drop(self.watcher.take());
if let Some(project_dir) = &state.config.project_dir {
match build_globset(&state.config.watch_patterns)
.map_err(anyhow::Error::new)
.and_then(|globset| {
create_watcher(ctx.clone(), self.modified.clone(), project_dir, globset)
.map_err(anyhow::Error::new)
}) {
Ok(watcher) => self.watcher = Some(watcher),
Err(e) => log::error!("Failed to create watcher: {e}"),
}
state.watcher_change = false;
}
}
if state.obj_change {
*diff_state = Default::default();
if state.config.selected_obj.is_some() {
state.queue_build = true;
}
state.obj_change = false;
}
if self.modified.swap(false, Ordering::Relaxed) && state.config.rebuild_on_changes {
state.queue_build = true;
}
if let Some(result) = &diff_state.build {
if mod_check {
if let Some((obj, _)) = &result.first_obj {
if let (Some(path), Some(timestamp)) = (&obj.path, obj.timestamp) {
if file_modified(path, timestamp) {
state.queue_reload = true;
}
}
}
if let Some((obj, _)) = &result.second_obj {
if let (Some(path), Some(timestamp)) = (&obj.path, obj.timestamp) {
if file_modified(path, timestamp) {
state.queue_reload = true;
}
}
}
}
}
// Don't clear `queue_build` if a build is running. A file may have been modified during
// the build, so we'll start another build after the current one finishes.
if state.queue_build
&& state.config.selected_obj.is_some()
&& !jobs.is_running(Job::ObjDiff)
{
jobs.push(start_build(ctx, ObjDiffConfig::from_state(state)));
state.queue_build = false;
state.queue_reload = false;
} else if state.queue_reload && !jobs.is_running(Job::ObjDiff) {
let mut diff_config = ObjDiffConfig::from_state(state);
// Don't build, just reload the current files
diff_config.build_base = false;
diff_config.build_target = false;
jobs.push(start_build(ctx, diff_config));
state.queue_reload = false;
}
if graphics_state.should_relaunch {
if let Some(app_path) = &self.app_path {
if let Ok(mut guard) = self.relaunch_path.lock() {
*guard = Some(app_path.clone());
self.should_relaunch = true;
}
}
}
}
}
impl eframe::App for App {
/// Called each time the UI needs repainting, which may be many times per second.
/// Put your widgets into a `SidePanel`, `TopPanel`, `CentralPanel`, `Window` or `Area`.
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
if self.should_relaunch {
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
return;
}
self.pre_update(ctx);
let Self { state, appearance, view_state, .. } = self;
let ViewState {
jobs,
config_state,
demangle_state,
rlwinm_decode_state,
diff_state,
graphics_state,
frame_history,
show_appearance_config,
show_demangle,
show_rlwinm_decode,
show_project_config,
show_arch_config,
show_debug,
show_graphics,
show_jobs,
show_side_panel,
} = view_state;
frame_history.on_new_frame(ctx.input(|i| i.time), frame.info().cpu_usage);
let side_panel_available = diff_state.current_view == View::SymbolDiff;
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
egui::menu::bar(ui, |ui| {
if ui
.add_enabled(
side_panel_available,
egui::Button::new(if *show_side_panel { "" } else { "" }),
)
.on_hover_text("Toggle side panel")
.clicked()
{
*show_side_panel = !*show_side_panel;
}
ui.separator();
ui.menu_button("File", |ui| {
#[cfg(debug_assertions)]
if ui.button("Debug…").clicked() {
*show_debug = !*show_debug;
ui.close_menu();
}
if ui.button("Project…").clicked() {
*show_project_config = !*show_project_config;
ui.close_menu();
}
let recent_projects = if let Ok(guard) = state.read() {
guard.config.recent_projects.clone()
} else {
vec![]
};
if recent_projects.is_empty() {
ui.add_enabled(false, egui::Button::new("Recent projects…"));
} else {
ui.menu_button("Recent Projects…", |ui| {
if ui.button("Clear").clicked() {
state.write().unwrap().config.recent_projects.clear();
};
ui.separator();
for path in recent_projects {
if ui.button(format!("{}", path.display())).clicked() {
state.write().unwrap().set_project_dir(path);
ui.close_menu();
}
}
});
}
if ui.button("Appearance…").clicked() {
*show_appearance_config = !*show_appearance_config;
ui.close_menu();
}
if ui.button("Graphics…").clicked() {
*show_graphics = !*show_graphics;
ui.close_menu();
}
if ui.button("Quit").clicked() {
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
}
});
ui.menu_button("Tools", |ui| {
if ui.button("Demangle…").clicked() {
*show_demangle = !*show_demangle;
ui.close_menu();
}
if ui.button("Rlwinm Decoder…").clicked() {
*show_rlwinm_decode = !*show_rlwinm_decode;
ui.close_menu();
}
});
ui.menu_button("Diff Options", |ui| {
if ui.button("Arch Settings…").clicked() {
*show_arch_config = !*show_arch_config;
ui.close_menu();
}
let mut state = state.write().unwrap();
let response = ui
.checkbox(&mut state.config.rebuild_on_changes, "Rebuild on changes")
.on_hover_text("Automatically re-run the build & diff when files change.");
if response.changed() {
state.watcher_change = true;
};
ui.add_enabled(
!diff_state.symbol_state.disable_reverse_fn_order,
egui::Checkbox::new(
&mut diff_state.symbol_state.reverse_fn_order,
"Reverse function order (-inline deferred)",
),
)
.on_disabled_hover_text(CONFIG_DISABLED_TEXT);
ui.checkbox(
&mut diff_state.symbol_state.show_hidden_symbols,
"Show hidden symbols",
);
if ui
.checkbox(
&mut state.config.diff_obj_config.relax_reloc_diffs,
"Relax relocation diffs",
)
.on_hover_text(
"Ignores differences in relocation targets. (Address, name, etc)",
)
.changed()
{
state.queue_reload = true;
}
if ui
.checkbox(
&mut state.config.diff_obj_config.space_between_args,
"Space between args",
)
.changed()
{
state.queue_reload = true;
}
if ui
.checkbox(
&mut state.config.diff_obj_config.combine_data_sections,
"Combine data sections",
)
.on_hover_text("Combines data sections with equal names.")
.changed()
{
state.queue_reload = true;
}
if ui.button("Clear custom symbol mappings").clicked() {
state.clear_mappings();
diff_state.post_build_nav = Some(DiffViewNavigation::symbol_diff());
state.queue_reload = true;
}
});
ui.separator();
if jobs_menu_ui(ui, jobs, appearance) {
*show_jobs = !*show_jobs;
}
});
});
if side_panel_available {
egui::SidePanel::left("side_panel").show_animated(ctx, *show_side_panel, |ui| {
egui::ScrollArea::both().show(ui, |ui| {
config_ui(ui, state, show_project_config, config_state, appearance);
});
});
}
let mut action = None;
egui::CentralPanel::default().show(ctx, |ui| {
let build_success = matches!(&diff_state.build, Some(b) if b.first_status.success && b.second_status.success);
action = if diff_state.current_view == View::FunctionDiff && build_success {
function_diff_ui(ui, diff_state, appearance)
} else if diff_state.current_view == View::DataDiff && build_success {
data_diff_ui(ui, diff_state, appearance)
} else if diff_state.current_view == View::ExtabDiff && build_success {
extab_diff_ui(ui, diff_state, appearance)
} else {
symbol_diff_ui(ui, diff_state, appearance)
};
});
project_window(ctx, state, show_project_config, config_state, appearance);
appearance_window(ctx, show_appearance_config, appearance);
demangle_window(ctx, show_demangle, demangle_state, appearance);
rlwinm_decode_window(ctx, show_rlwinm_decode, rlwinm_decode_state, appearance);
arch_config_window(ctx, state, show_arch_config, appearance);
debug_window(ctx, show_debug, frame_history, appearance);
graphics_window(ctx, show_graphics, frame_history, graphics_state, appearance);
jobs_window(ctx, show_jobs, jobs, appearance);
self.post_update(ctx, action);
}
/// Called by the framework to save state before shutdown.
fn save(&mut self, storage: &mut dyn eframe::Storage) {
if let Ok(state) = self.state.read() {
eframe::set_value(storage, CONFIG_KEY, &state.config);
}
eframe::set_value(storage, APPEARANCE_KEY, &self.appearance);
}
}
fn create_watcher(
ctx: egui::Context,
modified: Arc<AtomicBool>,
project_dir: &Path,
patterns: GlobSet,
) -> notify::Result<notify::RecommendedWatcher> {
let base_dir = project_dir.to_owned();
let mut watcher =
notify::recommended_watcher(move |res: notify::Result<notify::Event>| match res {
Ok(event) => {
if matches!(
event.kind,
notify::EventKind::Modify(..)
| notify::EventKind::Create(..)
| notify::EventKind::Remove(..)
) {
for path in &event.paths {
let Ok(path) = path.strip_prefix(&base_dir) else {
continue;
};
if patterns.is_match(path) {
log::info!("File modified: {}", path.display());
modified.store(true, Ordering::Relaxed);
ctx.request_repaint();
}
}
}
}
Err(e) => log::error!("watch error: {e:?}"),
})?;
watcher.watch(project_dir, RecursiveMode::Recursive)?;
Ok(watcher)
}
#[inline]
fn file_modified(path: &Path, last_ts: FileTime) -> bool {
if let Ok(metadata) = fs::metadata(path) {
FileTime::from_last_modification_time(&metadata) != last_ts
} else {
false
}
}

View File

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

127
objdiff-gui/src/config.rs Normal file
View File

@@ -0,0 +1,127 @@
use std::path::{Component, Path};
use anyhow::Result;
use globset::Glob;
use objdiff_core::config::{try_project_config, ProjectObject, DEFAULT_WATCH_PATTERNS};
use crate::app::{AppState, ObjectConfig};
#[derive(Clone)]
pub enum ProjectObjectNode {
Unit(String, usize),
Dir(String, Vec<ProjectObjectNode>),
}
fn join_single_dir_entries(nodes: &mut Vec<ProjectObjectNode>) {
for node in nodes {
if let ProjectObjectNode::Dir(my_name, my_nodes) = node {
join_single_dir_entries(my_nodes);
// If this directory consists of a single sub-directory...
if let [ProjectObjectNode::Dir(sub_name, sub_nodes)] = &mut my_nodes[..] {
// ... join the two names with a path separator and eliminate the layer
*my_name += "/";
*my_name += sub_name;
*my_nodes = std::mem::take(sub_nodes);
}
}
}
}
fn find_dir<'a>(
name: &str,
nodes: &'a mut Vec<ProjectObjectNode>,
) -> &'a mut Vec<ProjectObjectNode> {
if let Some(index) = nodes
.iter()
.position(|node| matches!(node, ProjectObjectNode::Dir(dir_name, _) if dir_name == name))
{
if let ProjectObjectNode::Dir(_, children) = &mut nodes[index] {
return children;
}
} else {
nodes.push(ProjectObjectNode::Dir(name.to_string(), vec![]));
if let Some(ProjectObjectNode::Dir(_, children)) = nodes.last_mut() {
return children;
}
}
unreachable!();
}
fn build_nodes(
units: &mut [ProjectObject],
project_dir: &Path,
target_obj_dir: Option<&Path>,
base_obj_dir: Option<&Path>,
) -> Vec<ProjectObjectNode> {
let mut nodes = vec![];
for (idx, unit) in units.iter_mut().enumerate() {
unit.resolve_paths(project_dir, target_obj_dir, base_obj_dir);
let mut out_nodes = &mut nodes;
let path = if let Some(name) = &unit.name {
Path::new(name)
} else if let Some(path) = &unit.path {
path
} else {
continue;
};
if let Some(parent) = path.parent() {
for component in parent.components() {
if let Component::Normal(name) = component {
let name = name.to_str().unwrap();
out_nodes = find_dir(name, out_nodes);
}
}
}
let filename = path.file_name().unwrap().to_str().unwrap().to_string();
out_nodes.push(ProjectObjectNode::Unit(filename, idx));
}
// Within the top-level module directories, join paths. Leave the
// top-level name intact though since it's the module name.
for node in &mut nodes {
if let ProjectObjectNode::Dir(_, sub_nodes) = node {
join_single_dir_entries(sub_nodes);
}
}
nodes
}
pub fn load_project_config(state: &mut AppState) -> Result<()> {
let Some(project_dir) = &state.config.project_dir else {
return Ok(());
};
if let Some((result, info)) = try_project_config(project_dir) {
let project_config = result?;
state.config.custom_make = project_config.custom_make.clone();
state.config.custom_args = project_config.custom_args.clone();
state.config.target_obj_dir =
project_config.target_dir.as_deref().map(|p| project_dir.join(p));
state.config.base_obj_dir = project_config.base_dir.as_deref().map(|p| project_dir.join(p));
state.config.build_base = project_config.build_base.unwrap_or(true);
state.config.build_target = project_config.build_target.unwrap_or(false);
state.config.watch_patterns = project_config.watch_patterns.clone().unwrap_or_else(|| {
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect()
});
state.watcher_change = true;
state.objects = project_config.units.clone().unwrap_or_default();
state.object_nodes = build_nodes(
&mut state.objects,
project_dir,
state.config.target_obj_dir.as_deref(),
state.config.base_obj_dir.as_deref(),
);
state.current_project_config = Some(project_config);
state.project_config_info = Some(info);
// Reload selected object
if let Some(selected_obj) = &state.config.selected_obj {
if let Some(obj) = state.objects.iter().find(|o| o.name() == selected_obj.name) {
let config = ObjectConfig::from(obj);
state.set_selected_obj(config);
} else {
state.clear_selected_obj();
}
}
}
Ok(())
}

View File

@@ -0,0 +1,146 @@
// font-kit/src/matching.rs
//
// Copyright © 2018 The Pathfinder Project Developers.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
//! Determines the closest font matching a description per the CSS Fonts Level 3 specification.
use float_ord::FloatOrd;
use font_kit::{
error::SelectionError,
properties::{Properties, Stretch, Style, Weight},
};
/// This follows CSS Fonts Level 3 § 5.2 [1].
///
/// https://drafts.csswg.org/css-fonts-3/#font-style-matching
pub fn find_best_match(
candidates: &[Properties],
query: &Properties,
) -> Result<usize, SelectionError> {
// Step 4.
let mut matching_set: Vec<usize> = (0..candidates.len()).collect();
if matching_set.is_empty() {
return Err(SelectionError::NotFound);
}
// Step 4a (`font-stretch`).
let matching_stretch = if matching_set
.iter()
.any(|&index| candidates[index].stretch == query.stretch)
{
// Exact match.
query.stretch
} else if query.stretch <= Stretch::NORMAL {
// Closest width, first checking narrower values and then wider values.
match matching_set
.iter()
.filter(|&&index| candidates[index].stretch < query.stretch)
.min_by_key(|&&index| FloatOrd(query.stretch.0 - candidates[index].stretch.0))
{
Some(&matching_index) => candidates[matching_index].stretch,
None => {
let matching_index = *matching_set
.iter()
.min_by_key(|&&index| FloatOrd(candidates[index].stretch.0 - query.stretch.0))
.unwrap();
candidates[matching_index].stretch
}
}
} else {
// Closest width, first checking wider values and then narrower values.
match matching_set
.iter()
.filter(|&&index| candidates[index].stretch > query.stretch)
.min_by_key(|&&index| FloatOrd(candidates[index].stretch.0 - query.stretch.0))
{
Some(&matching_index) => candidates[matching_index].stretch,
None => {
let matching_index = *matching_set
.iter()
.min_by_key(|&&index| FloatOrd(query.stretch.0 - candidates[index].stretch.0))
.unwrap();
candidates[matching_index].stretch
}
}
};
matching_set.retain(|&index| candidates[index].stretch == matching_stretch);
// Step 4b (`font-style`).
let style_preference = match query.style {
Style::Italic => [Style::Italic, Style::Oblique, Style::Normal],
Style::Oblique => [Style::Oblique, Style::Italic, Style::Normal],
Style::Normal => [Style::Normal, Style::Oblique, Style::Italic],
};
let matching_style = *style_preference
.iter()
.find(|&query_style| {
matching_set.iter().any(|&index| candidates[index].style == *query_style)
})
.unwrap();
matching_set.retain(|&index| candidates[index].style == matching_style);
// Step 4c (`font-weight`).
//
// The spec doesn't say what to do if the weight is between 400 and 500 exclusive, so we
// just use 450 as the cutoff.
let matching_weight =
if matching_set.iter().any(|&index| candidates[index].weight == query.weight) {
query.weight
} else if query.weight >= Weight(400.0)
&& query.weight < Weight(450.0)
&& matching_set.iter().any(|&index| candidates[index].weight == Weight(500.0))
{
// Check 500 first.
Weight(500.0)
} else if query.weight >= Weight(450.0)
&& query.weight <= Weight(500.0)
&& matching_set.iter().any(|&index| candidates[index].weight == Weight(400.0))
{
// Check 400 first.
Weight(400.0)
} else if query.weight <= Weight(500.0) {
// Closest weight, first checking thinner values and then fatter ones.
match matching_set
.iter()
.filter(|&&index| candidates[index].weight <= query.weight)
.min_by_key(|&&index| FloatOrd(query.weight.0 - candidates[index].weight.0))
{
Some(&matching_index) => candidates[matching_index].weight,
None => {
let matching_index = *matching_set
.iter()
.min_by_key(|&&index| FloatOrd(candidates[index].weight.0 - query.weight.0))
.unwrap();
candidates[matching_index].weight
}
}
} else {
// Closest weight, first checking fatter values and then thinner ones.
match matching_set
.iter()
.filter(|&&index| candidates[index].weight >= query.weight)
.min_by_key(|&&index| FloatOrd(candidates[index].weight.0 - query.weight.0))
{
Some(&matching_index) => candidates[matching_index].weight,
None => {
let matching_index = *matching_set
.iter()
.min_by_key(|&&index| FloatOrd(query.weight.0 - candidates[index].weight.0))
.unwrap();
candidates[matching_index].weight
}
}
};
matching_set.retain(|&index| candidates[index].weight == matching_weight);
// Step 4d concerns `font-size`, but fonts in `font-kit` are unsized, so we ignore that.
// Return the result.
matching_set.into_iter().next().ok_or(SelectionError::NotFound)
}

View File

@@ -0,0 +1,107 @@
pub mod matching;
use std::{borrow::Cow, fs, sync::Arc};
use anyhow::{Context, Result};
use crate::fonts::matching::find_best_match;
pub struct LoadedFontFamily {
pub family_name: String,
pub fonts: Vec<font_kit::font::Font>,
pub handles: Vec<font_kit::handle::Handle>,
// pub properties: Vec<font_kit::properties::Properties>,
pub default_index: usize,
}
pub struct LoadedFont {
// pub font_name: String,
pub font_data: egui::FontData,
}
pub fn load_font_family(
source: &font_kit::source::SystemSource,
name: &str,
) -> Option<LoadedFontFamily> {
let family_handle = source.select_family_by_name(name).ok()?;
if family_handle.fonts().is_empty() {
log::warn!("No fonts found for family '{}'", name);
return None;
}
let handles = family_handle.fonts().to_vec();
let mut loaded = Vec::with_capacity(handles.len());
for handle in handles.iter() {
match font_kit::loaders::default::Font::from_handle(handle) {
Ok(font) => loaded.push(font),
Err(err) => {
log::warn!("Failed to load font '{}': {}", name, err);
return None;
}
}
}
let properties = loaded.iter().map(|f| f.properties()).collect::<Vec<_>>();
let default_index =
find_best_match(&properties, &font_kit::properties::Properties::new()).unwrap_or(0);
let font_family_name =
loaded.first().map(|f| f.family_name()).unwrap_or_else(|| name.to_string());
Some(LoadedFontFamily {
family_name: font_family_name,
fonts: loaded,
handles,
// properties,
default_index,
})
}
pub fn load_font(handle: &font_kit::handle::Handle) -> Result<LoadedFont> {
// let loaded = font_kit::loaders::default::Font::from_handle(handle)?;
let data = match handle {
font_kit::handle::Handle::Memory { bytes, font_index } => egui::FontData {
font: Cow::Owned(bytes.to_vec()),
index: *font_index,
tweak: Default::default(),
},
font_kit::handle::Handle::Path { path, font_index } => {
let vec = fs::read(path).with_context(|| {
format!("Failed to load font '{}' (index {})", path.display(), font_index)
})?;
egui::FontData { font: Cow::Owned(vec), index: *font_index, tweak: Default::default() }
}
};
Ok(LoadedFont {
// font_name: loaded.full_name(),
font_data: data,
})
}
pub fn load_font_if_needed(
ctx: &egui::Context,
source: &font_kit::source::SystemSource,
font_id: &egui::FontId,
base_family: egui::FontFamily,
fonts: &mut egui::FontDefinitions,
) -> Result<()> {
if fonts.families.contains_key(&font_id.family) {
return Ok(());
}
let family_name = match &font_id.family {
egui::FontFamily::Proportional | egui::FontFamily::Monospace => return Ok(()),
egui::FontFamily::Name(v) => v,
};
let family = load_font_family(source, family_name)
.with_context(|| format!("Failed to load font family '{}'", family_name))?;
let default_fonts = fonts.families.get(&base_family).cloned().unwrap_or_default();
// FIXME clean up
let default_font_ref = family.fonts.get(family.default_index).unwrap();
let default_font = family.handles.get(family.default_index).unwrap();
let default_font_data = load_font(default_font).unwrap();
log::info!("Loaded font family '{}'", family.family_name);
fonts.font_data.insert(default_font_ref.full_name(), default_font_data.font_data);
fonts
.families
.entry(egui::FontFamily::Name(Arc::from(family.family_name)))
.or_insert_with(|| default_fonts)
.insert(0, default_font_ref.full_name());
ctx.set_fonts(fonts.clone());
Ok(())
}

View File

@@ -0,0 +1,39 @@
use std::sync::mpsc::Receiver;
use anyhow::{Context, Result};
use self_update::{cargo_crate_version, update::Release};
use crate::{
jobs::{start_job, update_status, Job, JobContext, JobResult, JobState},
update::{build_updater, BIN_NAME_NEW, BIN_NAME_OLD},
};
pub struct CheckUpdateResult {
pub update_available: bool,
pub latest_release: Release,
pub found_binary: Option<String>,
}
fn run_check_update(context: &JobContext, cancel: Receiver<()>) -> Result<Box<CheckUpdateResult>> {
update_status(context, "Fetching latest release".to_string(), 0, 1, &cancel)?;
let updater = build_updater().context("Failed to create release updater")?;
let latest_release = updater.get_latest_release()?;
let update_available =
self_update::version::bump_is_greater(cargo_crate_version!(), &latest_release.version)?;
// Find the binary name in the release assets
let found_binary = latest_release
.assets
.iter()
.find(|a| a.name == BIN_NAME_NEW)
.or_else(|| latest_release.assets.iter().find(|a| a.name == BIN_NAME_OLD))
.map(|a| a.name.clone());
update_status(context, "Complete".to_string(), 1, 1, &cancel)?;
Ok(Box::new(CheckUpdateResult { update_available, latest_release, found_binary }))
}
pub fn start_check_update(ctx: &egui::Context) -> JobState {
start_job(ctx, "Check for updates", Job::CheckUpdate, move |context, cancel| {
run_check_update(&context, cancel).map(|result| JobResult::CheckUpdate(Some(result)))
})
}

View File

@@ -0,0 +1,139 @@
use std::{fs, path::PathBuf, sync::mpsc::Receiver};
use anyhow::{anyhow, bail, Context, Result};
use const_format::formatcp;
use crate::{
app::AppConfig,
jobs::{
objdiff::{run_make, BuildConfig, BuildStatus},
start_job, update_status, Job, JobContext, JobResult, JobState,
},
};
#[derive(Debug, Clone)]
pub struct CreateScratchConfig {
pub build_config: BuildConfig,
pub context_path: Option<PathBuf>,
pub build_context: bool,
// Scratch fields
pub compiler: String,
pub platform: String,
pub compiler_flags: String,
pub function_name: String,
pub target_obj: PathBuf,
pub preset_id: Option<u32>,
}
impl CreateScratchConfig {
pub(crate) fn from_config(config: &AppConfig, function_name: String) -> Result<Self> {
let Some(selected_obj) = &config.selected_obj else {
bail!("No object selected");
};
let Some(target_path) = &selected_obj.target_path else {
bail!("No target path for {}", selected_obj.name);
};
let Some(scratch_config) = &selected_obj.scratch else {
bail!("No scratch configuration for {}", selected_obj.name);
};
Ok(Self {
build_config: BuildConfig::from_config(config),
context_path: scratch_config.ctx_path.clone(),
build_context: scratch_config.build_ctx.unwrap_or(false),
compiler: scratch_config.compiler.clone().unwrap_or_default(),
platform: scratch_config.platform.clone().unwrap_or_default(),
compiler_flags: scratch_config.c_flags.clone().unwrap_or_default(),
function_name,
target_obj: target_path.to_path_buf(),
preset_id: scratch_config.preset_id,
})
}
pub fn is_available(config: &AppConfig) -> bool {
let Some(selected_obj) = &config.selected_obj else {
return false;
};
selected_obj.target_path.is_some() && selected_obj.scratch.is_some()
}
}
#[derive(Default, Debug, Clone)]
pub struct CreateScratchResult {
pub scratch_url: String,
}
#[derive(Debug, Default, Clone, serde::Deserialize)]
struct CreateScratchResponse {
pub slug: String,
pub claim_token: String,
}
const API_HOST: &str = "https://decomp.me";
fn run_create_scratch(
status: &JobContext,
cancel: Receiver<()>,
config: CreateScratchConfig,
) -> Result<Box<CreateScratchResult>> {
let project_dir =
config.build_config.project_dir.as_ref().ok_or_else(|| anyhow!("Missing project dir"))?;
let mut context = None;
if let Some(context_path) = &config.context_path {
if config.build_context {
update_status(status, "Building context".to_string(), 0, 2, &cancel)?;
match run_make(&config.build_config, context_path) {
BuildStatus { success: true, .. } => {}
BuildStatus { success: false, stdout, stderr, .. } => {
bail!("Failed to build context:\n{stdout}\n{stderr}")
}
}
}
let context_path = project_dir.join(context_path);
context = Some(
fs::read_to_string(&context_path)
.map_err(|e| anyhow!("Failed to read {}: {}", context_path.display(), e))?,
);
}
update_status(status, "Creating scratch".to_string(), 1, 2, &cancel)?;
let diff_flags = [format!("--disassemble={}", config.function_name)];
let diff_flags = serde_json::to_string(&diff_flags).unwrap();
let obj_path = project_dir.join(&config.target_obj);
let file = reqwest::blocking::multipart::Part::file(&obj_path)
.with_context(|| format!("Failed to open {}", obj_path.display()))?;
let mut form = reqwest::blocking::multipart::Form::new()
.text("compiler", config.compiler.clone())
.text("platform", config.platform.clone())
.text("compiler_flags", config.compiler_flags.clone())
.text("diff_label", config.function_name.clone())
.text("diff_flags", diff_flags)
.text("context", context.unwrap_or_default())
.text("source_code", "// Move related code from Context tab to here");
if let Some(preset) = config.preset_id {
form = form.text("preset", preset.to_string());
}
form = form.part("target_obj", file);
let client = reqwest::blocking::Client::new();
let response = client
.post(formatcp!("{API_HOST}/api/scratch"))
.multipart(form)
.send()
.map_err(|e| anyhow!("Failed to send request: {}", e))?;
if !response.status().is_success() {
return Err(anyhow!("Failed to create scratch: {}", response.text()?));
}
let body: CreateScratchResponse = response.json().context("Failed to parse response")?;
let scratch_url = format!("{API_HOST}/scratch/{}/claim?token={}", body.slug, body.claim_token);
update_status(status, "Complete".to_string(), 2, 2, &cancel)?;
Ok(Box::from(CreateScratchResult { scratch_url }))
}
pub fn start_create_scratch(ctx: &egui::Context, config: CreateScratchConfig) -> JobState {
start_job(ctx, "Create scratch", Job::CreateScratch, move |context, cancel| {
run_create_scratch(&context, cancel, config)
.map(|result| JobResult::CreateScratch(Some(result)))
})
}

189
objdiff-gui/src/jobs/mod.rs Normal file
View File

@@ -0,0 +1,189 @@
use std::{
sync::{
atomic::{AtomicUsize, Ordering},
mpsc::{Receiver, Sender, TryRecvError},
Arc, RwLock,
},
thread::JoinHandle,
};
use anyhow::Result;
use crate::jobs::{
check_update::CheckUpdateResult, create_scratch::CreateScratchResult, objdiff::ObjDiffResult,
update::UpdateResult,
};
pub mod check_update;
pub mod create_scratch;
pub mod objdiff;
pub mod update;
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
pub enum Job {
ObjDiff,
CheckUpdate,
Update,
CreateScratch,
}
pub static JOB_ID: AtomicUsize = AtomicUsize::new(0);
#[derive(Default)]
pub struct JobQueue {
pub jobs: Vec<JobState>,
pub results: Vec<JobResult>,
}
impl JobQueue {
/// Adds a job to the queue.
#[inline]
pub fn push(&mut self, state: JobState) { self.jobs.push(state); }
/// Adds a job to the queue if a job of the given kind is not already running.
#[inline]
pub fn push_once(&mut self, job: Job, func: impl FnOnce() -> JobState) {
if !self.is_running(job) {
self.push(func());
}
}
/// Returns whether a job of the given kind is running.
pub fn is_running(&self, kind: Job) -> bool {
self.jobs.iter().any(|j| j.kind == kind && j.handle.is_some())
}
/// Returns whether any job is running.
#[expect(dead_code)]
pub fn any_running(&self) -> bool {
self.jobs.iter().any(|job| {
if let Some(handle) = &job.handle {
return !handle.is_finished();
}
false
})
}
/// Iterates over all jobs mutably.
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut JobState> + '_ { self.jobs.iter_mut() }
/// Iterates over all finished jobs, returning the job state and the result.
pub fn iter_finished(
&mut self,
) -> impl Iterator<Item = (&mut JobState, std::thread::Result<JobResult>)> + '_ {
self.jobs.iter_mut().filter_map(|job| {
if let Some(handle) = &job.handle {
if !handle.is_finished() {
return None;
}
let result = job.handle.take().unwrap().join();
return Some((job, result));
}
None
})
}
/// Clears all finished jobs.
pub fn clear_finished(&mut self) {
self.jobs.retain(|job| {
!(job.handle.is_none() && job.context.status.read().unwrap().error.is_none())
});
}
/// Clears all errored jobs.
pub fn clear_errored(&mut self) {
self.jobs.retain(|job| job.context.status.read().unwrap().error.is_none());
}
/// Removes a job from the queue given its ID.
pub fn remove(&mut self, id: usize) { self.jobs.retain(|job| job.id != id); }
}
#[derive(Clone)]
pub struct JobContext {
pub status: Arc<RwLock<JobStatus>>,
pub egui: egui::Context,
}
pub struct JobState {
pub id: usize,
pub kind: Job,
pub handle: Option<JoinHandle<JobResult>>,
pub context: JobContext,
pub cancel: Sender<()>,
}
#[derive(Default)]
pub struct JobStatus {
pub title: String,
pub progress_percent: f32,
pub progress_items: Option<[u32; 2]>,
pub status: String,
pub error: Option<anyhow::Error>,
}
pub enum JobResult {
None,
ObjDiff(Option<Box<ObjDiffResult>>),
CheckUpdate(Option<Box<CheckUpdateResult>>),
Update(Box<UpdateResult>),
CreateScratch(Option<Box<CreateScratchResult>>),
}
fn should_cancel(rx: &Receiver<()>) -> bool {
match rx.try_recv() {
Ok(_) | Err(TryRecvError::Disconnected) => true,
Err(_) => false,
}
}
fn start_job(
ctx: &egui::Context,
title: &str,
kind: Job,
run: impl FnOnce(JobContext, Receiver<()>) -> Result<JobResult> + Send + 'static,
) -> JobState {
let status = Arc::new(RwLock::new(JobStatus {
title: title.to_string(),
progress_percent: 0.0,
progress_items: None,
status: String::new(),
error: None,
}));
let context = JobContext { status: status.clone(), egui: ctx.clone() };
let context_inner = JobContext { status: status.clone(), egui: ctx.clone() };
let (tx, rx) = std::sync::mpsc::channel();
let handle = std::thread::spawn(move || match run(context_inner, rx) {
Ok(state) => state,
Err(e) => {
if let Ok(mut w) = status.write() {
w.error = Some(e);
}
JobResult::None
}
});
let id = JOB_ID.fetch_add(1, Ordering::Relaxed);
log::info!("Started job {}", id);
JobState { id, kind, handle: Some(handle), context, cancel: tx }
}
fn update_status(
context: &JobContext,
str: String,
count: u32,
total: u32,
cancel: &Receiver<()>,
) -> Result<()> {
let mut w =
context.status.write().map_err(|_| anyhow::Error::msg("Failed to lock job status"))?;
w.progress_items = Some([count, total]);
w.progress_percent = count as f32 / total as f32;
if should_cancel(cancel) {
w.status = "Cancelled".to_string();
return Err(anyhow::Error::msg("Cancelled"));
} else {
w.status = str;
}
drop(w);
context.egui.request_repaint();
Ok(())
}

View File

@@ -0,0 +1,328 @@
use std::{
path::{Path, PathBuf},
process::Command,
sync::mpsc::Receiver,
};
use anyhow::{anyhow, Error, Result};
use objdiff_core::{
diff::{diff_objs, DiffObjConfig, MappingConfig, ObjDiff},
obj::{read, ObjInfo},
};
use time::OffsetDateTime;
use crate::{
app::{AppConfig, AppState, ObjectConfig},
jobs::{start_job, update_status, Job, JobContext, JobResult, JobState},
};
pub struct BuildStatus {
pub success: bool,
pub cmdline: String,
pub stdout: String,
pub stderr: String,
}
impl Default for BuildStatus {
fn default() -> Self {
BuildStatus {
success: true,
cmdline: String::new(),
stdout: String::new(),
stderr: String::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct BuildConfig {
pub project_dir: Option<PathBuf>,
pub custom_make: Option<String>,
pub custom_args: Option<Vec<String>>,
#[allow(unused)]
pub selected_wsl_distro: Option<String>,
}
impl BuildConfig {
pub(crate) fn from_config(config: &AppConfig) -> Self {
Self {
project_dir: config.project_dir.clone(),
custom_make: config.custom_make.clone(),
custom_args: config.custom_args.clone(),
selected_wsl_distro: config.selected_wsl_distro.clone(),
}
}
}
pub struct ObjDiffConfig {
pub build_config: BuildConfig,
pub build_base: bool,
pub build_target: bool,
pub selected_obj: Option<ObjectConfig>,
pub diff_obj_config: DiffObjConfig,
pub selecting_left: Option<String>,
pub selecting_right: Option<String>,
}
impl ObjDiffConfig {
pub(crate) fn from_state(state: &AppState) -> Self {
Self {
build_config: BuildConfig::from_config(&state.config),
build_base: state.config.build_base,
build_target: state.config.build_target,
selected_obj: state.config.selected_obj.clone(),
diff_obj_config: state.config.diff_obj_config.clone(),
selecting_left: state.selecting_left.clone(),
selecting_right: state.selecting_right.clone(),
}
}
}
pub struct ObjDiffResult {
pub first_status: BuildStatus,
pub second_status: BuildStatus,
pub first_obj: Option<(ObjInfo, ObjDiff)>,
pub second_obj: Option<(ObjInfo, ObjDiff)>,
pub time: OffsetDateTime,
}
pub(crate) fn run_make(config: &BuildConfig, arg: &Path) -> BuildStatus {
let Some(cwd) = &config.project_dir else {
return BuildStatus {
success: false,
stderr: "Missing project dir".to_string(),
..Default::default()
};
};
let make = config.custom_make.as_deref().unwrap_or("make");
let make_args = config.custom_args.as_deref().unwrap_or(&[]);
#[cfg(not(windows))]
let mut command = {
let mut command = Command::new(make);
command.current_dir(cwd).args(make_args).arg(arg);
command
};
#[cfg(windows)]
let mut command = {
use std::os::windows::process::CommandExt;
use path_slash::PathExt;
let mut command = if config.selected_wsl_distro.is_some() {
Command::new("wsl")
} else {
Command::new(make)
};
if let Some(distro) = &config.selected_wsl_distro {
// Strip distro root prefix \\wsl.localhost\{distro}
let wsl_path_prefix = format!("\\\\wsl.localhost\\{}", distro);
let cwd = match cwd.strip_prefix(wsl_path_prefix) {
Ok(new_cwd) => format!("/{}", new_cwd.to_slash_lossy().as_ref()),
Err(_) => cwd.to_string_lossy().to_string(),
};
command
.arg("--cd")
.arg(cwd)
.arg("-d")
.arg(distro)
.arg("--")
.arg(make)
.args(make_args)
.arg(arg.to_slash_lossy().as_ref());
} else {
command.current_dir(cwd).args(make_args).arg(arg.to_slash_lossy().as_ref());
}
command.creation_flags(winapi::um::winbase::CREATE_NO_WINDOW);
command
};
let mut cmdline = shell_escape::escape(command.get_program().to_string_lossy()).into_owned();
for arg in command.get_args() {
cmdline.push(' ');
cmdline.push_str(shell_escape::escape(arg.to_string_lossy()).as_ref());
}
let output = match command.output() {
Ok(output) => output,
Err(e) => {
return BuildStatus {
success: false,
cmdline,
stdout: Default::default(),
stderr: e.to_string(),
};
}
};
// Try from_utf8 first to avoid copying the buffer if it's valid, then fall back to from_utf8_lossy
let stdout = String::from_utf8(output.stdout)
.unwrap_or_else(|e| String::from_utf8_lossy(e.as_bytes()).into_owned());
let stderr = String::from_utf8(output.stderr)
.unwrap_or_else(|e| String::from_utf8_lossy(e.as_bytes()).into_owned());
BuildStatus { success: output.status.success(), cmdline, stdout, stderr }
}
fn run_build(
context: &JobContext,
cancel: Receiver<()>,
mut config: ObjDiffConfig,
) -> Result<Box<ObjDiffResult>> {
let obj_config = config.selected_obj.ok_or_else(|| Error::msg("Missing obj path"))?;
// Use the per-object symbol mappings, we don't set mappings globally
config.diff_obj_config.symbol_mappings = MappingConfig {
mappings: obj_config.symbol_mappings,
selecting_left: config.selecting_left,
selecting_right: config.selecting_right,
};
let project_dir = config
.build_config
.project_dir
.as_ref()
.ok_or_else(|| Error::msg("Missing project dir"))?;
let target_path_rel = if let Some(target_path) = &obj_config.target_path {
Some(target_path.strip_prefix(project_dir).map_err(|_| {
anyhow!(
"Target path '{}' doesn't begin with '{}'",
target_path.display(),
project_dir.display()
)
})?)
} else {
None
};
let base_path_rel = if let Some(base_path) = &obj_config.base_path {
Some(base_path.strip_prefix(project_dir).map_err(|_| {
anyhow!(
"Base path '{}' doesn't begin with '{}'",
base_path.display(),
project_dir.display()
)
})?)
} else {
None
};
let mut total = 1;
if config.build_target && target_path_rel.is_some() {
total += 1;
}
if config.build_base && base_path_rel.is_some() {
total += 1;
}
if target_path_rel.is_some() {
total += 1;
}
if base_path_rel.is_some() {
total += 1;
}
let mut step_idx = 0;
let mut first_status = match target_path_rel {
Some(target_path_rel) if config.build_target => {
update_status(
context,
format!("Building target {}", target_path_rel.display()),
step_idx,
total,
&cancel,
)?;
step_idx += 1;
run_make(&config.build_config, target_path_rel)
}
_ => BuildStatus::default(),
};
let mut second_status = match base_path_rel {
Some(base_path_rel) if config.build_base => {
update_status(
context,
format!("Building base {}", base_path_rel.display()),
step_idx,
total,
&cancel,
)?;
step_idx += 1;
run_make(&config.build_config, base_path_rel)
}
_ => BuildStatus::default(),
};
let time = OffsetDateTime::now_utc();
let first_obj = match &obj_config.target_path {
Some(target_path) if first_status.success => {
update_status(
context,
format!("Loading target {}", target_path_rel.unwrap().display()),
step_idx,
total,
&cancel,
)?;
step_idx += 1;
match read::read(target_path, &config.diff_obj_config) {
Ok(obj) => Some(obj),
Err(e) => {
first_status = BuildStatus {
success: false,
stdout: format!("Loading object '{}'", target_path.display()),
stderr: format!("{:#}", e),
..Default::default()
};
None
}
}
}
Some(_) => {
step_idx += 1;
None
}
_ => None,
};
let second_obj = match &obj_config.base_path {
Some(base_path) if second_status.success => {
update_status(
context,
format!("Loading base {}", base_path_rel.unwrap().display()),
step_idx,
total,
&cancel,
)?;
step_idx += 1;
match read::read(base_path, &config.diff_obj_config) {
Ok(obj) => Some(obj),
Err(e) => {
second_status = BuildStatus {
success: false,
stdout: format!("Loading object '{}'", base_path.display()),
stderr: format!("{:#}", e),
..Default::default()
};
None
}
}
}
Some(_) => {
step_idx += 1;
None
}
_ => None,
};
update_status(context, "Performing diff".to_string(), step_idx, total, &cancel)?;
step_idx += 1;
let result = diff_objs(&config.diff_obj_config, first_obj.as_ref(), second_obj.as_ref(), None)?;
update_status(context, "Complete".to_string(), step_idx, total, &cancel)?;
Ok(Box::new(ObjDiffResult {
first_status,
second_status,
first_obj: first_obj.and_then(|o| result.left.map(|d| (o, d))),
second_obj: second_obj.and_then(|o| result.right.map(|d| (o, d))),
time,
}))
}
pub fn start_build(ctx: &egui::Context, config: ObjDiffConfig) -> JobState {
start_job(ctx, "Build", Job::ObjDiff, move |context, cancel| {
run_build(&context, cancel, config).map(|result| JobResult::ObjDiff(Some(result)))
})
}

View File

@@ -1,32 +1,34 @@
use std::{
env::{current_dir, current_exe},
fs,
fs::File,
path::PathBuf,
sync::mpsc::Receiver,
};
use anyhow::{Context, Result};
use const_format::formatcp;
use crate::{
jobs::{queue_job, update_status, Job, JobResult, JobState, Status},
update::{build_updater, BIN_NAME},
jobs::{start_job, update_status, Job, JobContext, JobResult, JobState},
update::build_updater,
};
pub struct UpdateResult {
pub exe_path: PathBuf,
}
fn run_update(status: &Status, cancel: Receiver<()>) -> Result<Box<UpdateResult>> {
fn run_update(
status: &JobContext,
cancel: Receiver<()>,
bin_name: String,
) -> Result<Box<UpdateResult>> {
update_status(status, "Fetching latest release".to_string(), 0, 3, &cancel)?;
let updater = build_updater().context("Failed to create release updater")?;
let latest_release = updater.get_latest_release()?;
let asset = latest_release
.assets
.iter()
.find(|a| a.name == BIN_NAME)
.ok_or_else(|| anyhow::Error::msg(formatcp!("No release asset for {}", BIN_NAME)))?;
.find(|a| a.name == bin_name)
.ok_or_else(|| anyhow::Error::msg(format!("No release asset for {bin_name}")))?;
update_status(status, "Downloading release".to_string(), 1, 3, &cancel)?;
let tmp_dir = tempfile::Builder::new().prefix("update").tempdir_in(current_dir()?)?;
@@ -34,7 +36,7 @@ fn run_update(status: &Status, cancel: Receiver<()>) -> Result<Box<UpdateResult>
let tmp_file = File::create(&tmp_path)?;
self_update::Download::from_url(&asset.download_url)
.set_header(reqwest::header::ACCEPT, "application/octet-stream".parse()?)
.download_to(&tmp_file)?;
.download_to(tmp_file)?;
update_status(status, "Extracting release".to_string(), 2, 3, &cancel)?;
let tmp_file = tmp_dir.path().join("replacement_tmp");
@@ -44,18 +46,19 @@ fn run_update(status: &Status, cancel: Receiver<()>) -> Result<Box<UpdateResult>
.to_dest(&target_file)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
use std::{fs, os::unix::fs::PermissionsExt};
let mut perms = fs::metadata(&target_file)?.permissions();
perms.set_mode(0o755);
fs::set_permissions(&target_file, perms)?;
}
tmp_dir.close()?;
update_status(status, "Complete".to_string(), 3, 3, &cancel)?;
Ok(Box::from(UpdateResult { exe_path: target_file }))
}
pub fn queue_update() -> JobState {
queue_job("Update app", Job::Update, move |status, cancel| {
run_update(status, cancel).map(JobResult::Update)
pub fn start_update(ctx: &egui::Context, bin_name: String) -> JobState {
start_job(ctx, "Update app", Job::Update, move |context, cancel| {
run_update(&context, cancel, bin_name).map(JobResult::Update)
})
}

231
objdiff-gui/src/main.rs Normal file
View File

@@ -0,0 +1,231 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
mod app;
mod app_config;
mod config;
mod fonts;
mod jobs;
mod update;
mod views;
use std::{
path::PathBuf,
process::ExitCode,
rc::Rc,
sync::{Arc, Mutex},
};
use anyhow::{ensure, Result};
use cfg_if::cfg_if;
use time::UtcOffset;
use tracing_subscriber::EnvFilter;
use crate::views::graphics::{load_graphics_config, GraphicsBackend, GraphicsConfig};
fn load_icon() -> Result<egui::IconData> {
use bytes::Buf;
let decoder = png::Decoder::new(include_bytes!("../assets/icon_64.png").reader());
let mut reader = decoder.read_info()?;
let mut buf = vec![0; reader.output_buffer_size()];
let info = reader.next_frame(&mut buf)?;
ensure!(info.bit_depth == png::BitDepth::Eight);
ensure!(info.color_type == png::ColorType::Rgba);
buf.truncate(info.buffer_size());
Ok(egui::IconData { rgba: buf, width: info.width, height: info.height })
}
const APP_NAME: &str = "objdiff";
// When compiling natively:
#[cfg(not(target_arch = "wasm32"))]
fn main() -> ExitCode {
// Log to stdout (if you run with `RUST_LOG=debug`).
tracing_subscriber::fmt()
.with_env_filter(
EnvFilter::builder()
// Default to info level
.with_default_directive(tracing_subscriber::filter::LevelFilter::INFO.into())
.from_env_lossy()
// This module is noisy at info level
.add_directive("wgpu_core::device::resource=warn".parse().unwrap()),
)
.init();
// Because localtime_r is unsound in multithreaded apps,
// we must call this before initializing eframe.
// https://github.com/time-rs/time/issues/293
let utc_offset = UtcOffset::current_local_offset().unwrap_or(UtcOffset::UTC);
let app_path = std::env::current_exe().ok();
let exec_path: Rc<Mutex<Option<PathBuf>>> = Rc::new(Mutex::new(None));
let mut native_options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default().with_app_id(APP_NAME),
..Default::default()
};
match load_icon() {
Ok(data) => {
native_options.viewport.icon = Some(Arc::new(data));
}
Err(e) => {
log::warn!("Failed to load application icon: {e:?}");
}
}
let mut graphics_config = GraphicsConfig::default();
let mut graphics_config_path = None;
if let Some(storage_dir) = eframe::storage_dir(APP_NAME) {
let config_path = storage_dir.join("graphics.ron");
match load_graphics_config(&config_path) {
Ok(Some(config)) => {
graphics_config = config;
}
Ok(None) => {}
Err(e) => {
log::error!("Failed to load native config: {e:?}");
}
}
graphics_config_path = Some(config_path);
}
#[cfg(feature = "wgpu")]
{
use eframe::egui_wgpu::wgpu::Backends;
if graphics_config.desired_backend.is_supported() {
native_options.wgpu_options.supported_backends = match graphics_config.desired_backend {
GraphicsBackend::Auto => native_options.wgpu_options.supported_backends,
GraphicsBackend::Dx12 => Backends::DX12,
GraphicsBackend::Metal => Backends::METAL,
GraphicsBackend::Vulkan => Backends::VULKAN,
GraphicsBackend::OpenGL => Backends::GL,
};
}
}
let mut eframe_error = None;
if let Err(e) = run_eframe(
native_options.clone(),
utc_offset,
exec_path.clone(),
app_path.clone(),
graphics_config.clone(),
graphics_config_path.clone(),
) {
eframe_error = Some(e);
}
#[cfg(feature = "wgpu")]
if let Some(e) = eframe_error {
// Attempt to relaunch using wgpu auto backend if the desired backend failed
#[allow(unused_mut)]
let mut should_relaunch = graphics_config.desired_backend != GraphicsBackend::Auto;
#[cfg(feature = "glow")]
{
// If the desired backend is OpenGL, we should try to relaunch using the glow renderer
should_relaunch &= graphics_config.desired_backend != GraphicsBackend::OpenGL;
}
if should_relaunch {
log::warn!("Failed to launch application: {e:?}");
log::warn!("Attempting to relaunch using auto-detected backend");
native_options.wgpu_options.supported_backends = Default::default();
if let Err(e) = run_eframe(
native_options.clone(),
utc_offset,
exec_path.clone(),
app_path.clone(),
graphics_config.clone(),
graphics_config_path.clone(),
) {
eframe_error = Some(e);
} else {
eframe_error = None;
}
} else {
eframe_error = Some(e);
}
}
#[cfg(all(feature = "wgpu", feature = "glow"))]
if let Some(e) = eframe_error {
// Attempt to relaunch using the glow renderer if the wgpu backend failed
log::warn!("Failed to launch application: {e:?}");
log::warn!("Attempting to relaunch using fallback OpenGL backend");
native_options.renderer = eframe::Renderer::Glow;
if let Err(e) = run_eframe(
native_options,
utc_offset,
exec_path.clone(),
app_path,
graphics_config,
graphics_config_path,
) {
eframe_error = Some(e);
} else {
eframe_error = None;
}
}
if let Some(e) = eframe_error {
log::error!("Failed to launch application: {e:?}");
return ExitCode::FAILURE;
}
// Attempt to relaunch application from the updated path
if let Ok(mut guard) = exec_path.lock() {
if let Some(path) = guard.take() {
cfg_if! {
if #[cfg(unix)] {
let e = exec::Command::new(path)
.args(&std::env::args().collect::<Vec<String>>())
.exec();
log::error!("Failed to relaunch: {e:?}");
return ExitCode::FAILURE;
} else {
let result = std::process::Command::new(path)
.args(std::env::args())
.spawn();
if let Err(e) = result {
log::error!("Failed to relaunch: {e:?}");
return ExitCode::FAILURE;
}
}
}
}
};
ExitCode::SUCCESS
}
fn run_eframe(
native_options: eframe::NativeOptions,
utc_offset: UtcOffset,
exec_path_clone: Rc<Mutex<Option<PathBuf>>>,
app_path: Option<PathBuf>,
graphics_config: GraphicsConfig,
graphics_config_path: Option<PathBuf>,
) -> Result<(), eframe::Error> {
eframe::run_native(
APP_NAME,
native_options,
Box::new(move |cc| {
Ok(Box::new(app::App::new(
cc,
utc_offset,
exec_path_clone,
app_path,
graphics_config,
graphics_config_path,
)))
}),
)
}
// when compiling to web using trunk.
#[cfg(target_arch = "wasm32")]
fn main() {
// Make sure panics are logged using `console.error`.
console_error_panic_hook::set_once();
// Redirect tracing to console.log and friends:
tracing_wasm::set_as_global_default();
let web_options = eframe::WebOptions::default();
eframe::start_web(
"the_canvas_id", // hardcode it
web_options,
Box::new(|cc| Box::new(eframe_template::TemplateApp::new(cc))),
)
.expect("failed to start eframe");
}

View File

@@ -20,8 +20,9 @@ cfg_if! {
}
pub const GITHUB_USER: &str = "encounter";
pub const GITHUB_REPO: &str = "objdiff";
pub const BIN_NAME: &str =
formatcp!("{}-{}-{}{}", GITHUB_REPO, OS, ARCH, std::env::consts::EXE_SUFFIX);
pub const BIN_NAME_NEW: &str =
formatcp!("objdiff-gui-{}-{}{}", OS, ARCH, std::env::consts::EXE_SUFFIX);
pub const BIN_NAME_OLD: &str = formatcp!("objdiff-{}-{}{}", OS, ARCH, std::env::consts::EXE_SUFFIX);
pub const RELEASE_URL: &str =
formatcp!("https://github.com/{}/{}/releases/latest", GITHUB_USER, GITHUB_REPO);
@@ -29,7 +30,8 @@ pub fn build_updater() -> self_update::errors::Result<Box<dyn ReleaseUpdate>> {
self_update::backends::github::Update::configure()
.repo_owner(GITHUB_USER)
.repo_name(GITHUB_REPO)
.bin_name(BIN_NAME)
// bin_name is required, but unused?
.bin_name(BIN_NAME_NEW)
.no_confirm(true)
.show_output(false)
.current_version(cargo_crate_version!())

View File

@@ -0,0 +1,313 @@
use std::sync::Arc;
use egui::{text::LayoutJob, Color32, FontFamily, FontId, TextFormat, TextStyle, Widget};
use time::UtcOffset;
use crate::fonts::load_font_if_needed;
#[derive(serde::Deserialize, serde::Serialize)]
#[serde(default)]
pub struct Appearance {
pub ui_font: FontId,
pub code_font: FontId,
pub diff_colors: Vec<Color32>,
pub theme: egui::Theme,
// Applied by theme
#[serde(skip)]
pub text_color: Color32, // GRAY
#[serde(skip)]
pub emphasized_text_color: Color32, // LIGHT_GRAY
#[serde(skip)]
pub deemphasized_text_color: Color32, // DARK_GRAY
#[serde(skip)]
pub highlight_color: Color32, // WHITE
#[serde(skip)]
pub replace_color: Color32, // LIGHT_BLUE
#[serde(skip)]
pub insert_color: Color32, // GREEN
#[serde(skip)]
pub delete_color: Color32, // RED
// Global
#[serde(skip)]
pub utc_offset: UtcOffset,
#[serde(skip)]
pub fonts: FontState,
#[serde(skip)]
pub next_ui_font: Option<FontId>,
#[serde(skip)]
pub next_code_font: Option<FontId>,
}
pub struct FontState {
definitions: egui::FontDefinitions,
source: font_kit::source::SystemSource,
family_names: Vec<String>,
// loaded_families: HashMap<String, LoadedFontFamily>,
}
const DEFAULT_UI_FONT: FontId = FontId { size: 12.0, family: FontFamily::Proportional };
const DEFAULT_CODE_FONT: FontId = FontId { size: 14.0, family: FontFamily::Monospace };
impl Default for Appearance {
fn default() -> Self {
Self {
ui_font: DEFAULT_UI_FONT,
code_font: DEFAULT_CODE_FONT,
diff_colors: DEFAULT_COLOR_ROTATION.to_vec(),
theme: egui::Theme::Dark,
text_color: Color32::GRAY,
emphasized_text_color: Color32::LIGHT_GRAY,
deemphasized_text_color: Color32::DARK_GRAY,
highlight_color: Color32::WHITE,
replace_color: Color32::LIGHT_BLUE,
insert_color: Color32::GREEN,
delete_color: Color32::from_rgb(200, 40, 41),
utc_offset: UtcOffset::UTC,
fonts: FontState::default(),
next_ui_font: None,
next_code_font: None,
}
}
}
impl Default for FontState {
fn default() -> Self {
Self {
definitions: Default::default(),
source: font_kit::source::SystemSource::new(),
family_names: Default::default(),
// loaded_families: Default::default(),
}
}
}
impl Appearance {
pub fn pre_update(&mut self, ctx: &egui::Context) {
let mut style = ctx.style().as_ref().clone();
style.text_styles.insert(TextStyle::Body, FontId {
size: (self.ui_font.size * 0.75).floor(),
family: self.ui_font.family.clone(),
});
style.text_styles.insert(TextStyle::Body, self.ui_font.clone());
style.text_styles.insert(TextStyle::Button, self.ui_font.clone());
style.text_styles.insert(TextStyle::Heading, FontId {
size: (self.ui_font.size * 1.5).floor(),
family: self.ui_font.family.clone(),
});
style.text_styles.insert(TextStyle::Monospace, self.code_font.clone());
match self.theme {
egui::Theme::Dark => {
style.visuals = egui::Visuals::dark();
self.text_color = Color32::GRAY;
self.emphasized_text_color = Color32::LIGHT_GRAY;
self.deemphasized_text_color = Color32::DARK_GRAY;
self.highlight_color = Color32::WHITE;
self.replace_color = Color32::LIGHT_BLUE;
self.insert_color = Color32::GREEN;
self.delete_color = Color32::from_rgb(200, 40, 41);
}
egui::Theme::Light => {
style.visuals = egui::Visuals::light();
self.text_color = Color32::GRAY;
self.emphasized_text_color = Color32::DARK_GRAY;
self.deemphasized_text_color = Color32::LIGHT_GRAY;
self.highlight_color = Color32::BLACK;
self.replace_color = Color32::DARK_BLUE;
self.insert_color = Color32::DARK_GREEN;
self.delete_color = Color32::from_rgb(200, 40, 41);
}
}
style.spacing.scroll = egui::style::ScrollStyle::solid();
style.spacing.scroll.bar_width = 10.0;
ctx.set_style(style);
}
pub fn post_update(&mut self, ctx: &egui::Context) {
// Load fonts for next frame
if let Some(next_ui_font) = self.next_ui_font.take() {
match load_font_if_needed(
ctx,
&self.fonts.source,
&next_ui_font,
DEFAULT_UI_FONT.family,
&mut self.fonts.definitions,
) {
Ok(()) => self.ui_font = next_ui_font,
Err(e) => {
log::error!("Failed to load font: {}", e)
}
}
}
if let Some(next_code_font) = self.next_code_font.take() {
match load_font_if_needed(
ctx,
&self.fonts.source,
&next_code_font,
DEFAULT_CODE_FONT.family,
&mut self.fonts.definitions,
) {
Ok(()) => self.code_font = next_code_font,
Err(e) => {
log::error!("Failed to load font: {}", e)
}
}
}
}
pub fn init_fonts(&mut self, ctx: &egui::Context) {
self.fonts.family_names = self.fonts.source.all_families().unwrap_or_default();
match load_font_if_needed(
ctx,
&self.fonts.source,
&self.ui_font,
DEFAULT_UI_FONT.family,
&mut self.fonts.definitions,
) {
Ok(_) => {}
Err(e) => {
log::error!("Failed to load font: {}", e);
// Revert to default
self.ui_font = DEFAULT_UI_FONT;
}
}
match load_font_if_needed(
ctx,
&self.fonts.source,
&self.code_font,
DEFAULT_CODE_FONT.family,
&mut self.fonts.definitions,
) {
Ok(_) => {}
Err(e) => {
log::error!("Failed to load font: {}", e);
// Revert to default
self.code_font = DEFAULT_CODE_FONT;
}
}
}
pub fn code_text_format(&self, base_color: Color32, highlight: bool) -> TextFormat {
TextFormat {
font_id: self.code_font.clone(),
color: if highlight { self.emphasized_text_color } else { base_color },
background: if highlight { self.deemphasized_text_color } else { Color32::TRANSPARENT },
..Default::default()
}
}
}
pub const DEFAULT_COLOR_ROTATION: [Color32; 9] = [
Color32::from_rgb(255, 0, 255),
Color32::from_rgb(0, 255, 255),
Color32::from_rgb(0, 128, 0),
Color32::from_rgb(255, 0, 0),
Color32::from_rgb(255, 255, 0),
Color32::from_rgb(255, 192, 203),
Color32::from_rgb(128, 128, 255),
Color32::from_rgb(0, 255, 0),
Color32::from_rgb(213, 138, 138),
];
fn font_id_ui(
ui: &mut egui::Ui,
label: &str,
mut font_id: FontId,
default: FontId,
appearance: &Appearance,
) -> Option<FontId> {
ui.push_id(label, |ui| {
let font_size = font_id.size;
let label_job = LayoutJob::simple(
font_id.family.to_string(),
font_id.clone(),
appearance.text_color,
0.0,
);
let mut changed = ui
.horizontal(|ui| {
ui.label(label);
let mut changed = egui::Slider::new(&mut font_id.size, 4.0..=40.0)
.max_decimals(1)
.ui(ui)
.changed();
if ui.button("Reset").clicked() {
font_id = default;
changed = true;
}
changed
})
.inner;
let family = &mut font_id.family;
changed |= egui::ComboBox::from_label("Font family")
.selected_text(label_job)
.width(font_size * 20.0)
.show_ui(ui, |ui| {
let mut result = false;
result |= ui
.selectable_value(family, FontFamily::Proportional, "Proportional (built-in)")
.changed();
result |= ui
.selectable_value(family, FontFamily::Monospace, "Monospace (built-in)")
.changed();
for family_name in &appearance.fonts.family_names {
result |= ui
.selectable_value(
family,
FontFamily::Name(Arc::from(family_name.as_str())),
family_name,
)
.changed();
}
result
})
.inner
.unwrap_or(false);
changed.then_some(font_id)
})
.inner
}
pub fn appearance_window(ctx: &egui::Context, show: &mut bool, appearance: &mut Appearance) {
egui::Window::new("Appearance").open(show).show(ctx, |ui| {
egui::ComboBox::from_label("Theme")
.selected_text(format!("{:?}", appearance.theme))
.show_ui(ui, |ui| {
ui.selectable_value(&mut appearance.theme, egui::Theme::Dark, "Dark");
ui.selectable_value(&mut appearance.theme, egui::Theme::Light, "Light");
});
ui.separator();
appearance.next_ui_font =
font_id_ui(ui, "UI font:", appearance.ui_font.clone(), DEFAULT_UI_FONT, appearance);
ui.separator();
appearance.next_code_font = font_id_ui(
ui,
"Code font:",
appearance.code_font.clone(),
DEFAULT_CODE_FONT,
appearance,
);
ui.separator();
ui.label("Diff colors:");
if ui.button("Reset").clicked() {
appearance.diff_colors = DEFAULT_COLOR_ROTATION.to_vec();
}
let mut remove_at: Option<usize> = None;
let num_colors = appearance.diff_colors.len();
for (idx, color) in appearance.diff_colors.iter_mut().enumerate() {
ui.horizontal(|ui| {
ui.color_edit_button_srgba(color);
if num_colors > 1 && ui.small_button("-").clicked() {
remove_at = Some(idx);
}
});
}
if let Some(idx) = remove_at {
appearance.diff_colors.remove(idx);
}
if ui.small_button("+").clicked() {
appearance.diff_colors.push(Color32::BLACK);
}
});
}

View File

@@ -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

@@ -0,0 +1,992 @@
#[cfg(all(windows, feature = "wsl"))]
use std::string::FromUtf16Error;
use std::{
mem::take,
path::{Path, PathBuf, MAIN_SEPARATOR},
};
#[cfg(all(windows, feature = "wsl"))]
use anyhow::{Context, Result};
use egui::{
output::OpenUrl, text::LayoutJob, CollapsingHeader, FontFamily, FontId, RichText,
SelectableLabel, TextFormat, Widget,
};
use globset::Glob;
use objdiff_core::{
config::{ProjectObject, DEFAULT_WATCH_PATTERNS},
diff::{ArmArchVersion, ArmR9Usage, MipsAbi, MipsInstrCategory, X86Formatter},
};
use strum::{EnumMessage, VariantArray};
use crate::{
app::{AppConfig, AppState, AppStateRef, ObjectConfig},
config::ProjectObjectNode,
jobs::{
check_update::{start_check_update, CheckUpdateResult},
update::start_update,
Job, JobQueue, JobResult,
},
update::RELEASE_URL,
views::{
appearance::Appearance,
file::{FileDialogResult, FileDialogState},
},
};
#[derive(Default)]
pub struct ConfigViewState {
pub check_update: Option<Box<CheckUpdateResult>>,
pub check_update_running: bool,
pub queue_check_update: bool,
pub update_running: bool,
pub queue_update: Option<String>,
pub build_running: bool,
pub queue_build: bool,
pub watch_pattern_text: String,
pub object_search: String,
pub filter_diffable: bool,
pub filter_incomplete: bool,
pub show_hidden: bool,
#[cfg(all(windows, feature = "wsl"))]
pub available_wsl_distros: Option<Vec<String>>,
pub file_dialog_state: FileDialogState,
}
impl ConfigViewState {
pub fn pre_update(&mut self, jobs: &mut JobQueue, state: &AppStateRef) {
jobs.results.retain_mut(|result| {
if let JobResult::CheckUpdate(result) = result {
self.check_update = take(result);
false
} else {
true
}
});
self.build_running = jobs.is_running(Job::ObjDiff);
self.check_update_running = jobs.is_running(Job::CheckUpdate);
self.update_running = jobs.is_running(Job::Update);
// Check async file dialog results
match self.file_dialog_state.poll() {
FileDialogResult::None => {}
FileDialogResult::ProjectDir(path) => {
let mut guard = state.write().unwrap();
guard.set_project_dir(path.to_path_buf());
}
FileDialogResult::TargetDir(path) => {
let mut guard = state.write().unwrap();
guard.set_target_obj_dir(path.to_path_buf());
}
FileDialogResult::BaseDir(path) => {
let mut guard = state.write().unwrap();
guard.set_base_obj_dir(path.to_path_buf());
}
FileDialogResult::Object(path) => {
let mut guard = state.write().unwrap();
if let (Some(base_dir), Some(target_dir)) =
(&guard.config.base_obj_dir, &guard.config.target_obj_dir)
{
if let Ok(obj_path) = path.strip_prefix(base_dir) {
let target_path = target_dir.join(obj_path);
guard.set_selected_obj(ObjectConfig {
name: obj_path.display().to_string(),
target_path: Some(target_path),
base_path: Some(path),
..Default::default()
});
} else if let Ok(obj_path) = path.strip_prefix(target_dir) {
let base_path = base_dir.join(obj_path);
guard.set_selected_obj(ObjectConfig {
name: obj_path.display().to_string(),
target_path: Some(path),
base_path: Some(base_path),
..Default::default()
});
}
}
}
}
}
pub fn post_update(&mut self, ctx: &egui::Context, jobs: &mut JobQueue, state: &AppStateRef) {
if self.queue_build {
self.queue_build = false;
if let Ok(mut state) = state.write() {
state.queue_build = true;
}
}
if self.queue_check_update {
self.queue_check_update = false;
jobs.push_once(Job::CheckUpdate, || start_check_update(ctx));
}
if let Some(bin_name) = self.queue_update.take() {
jobs.push_once(Job::Update, || start_update(ctx, bin_name));
}
}
}
#[cfg(all(windows, feature = "wsl"))]
fn process_utf16(bytes: &[u8]) -> Result<String, FromUtf16Error> {
let u16_bytes: Vec<u16> = bytes
.chunks_exact(2)
.filter_map(|c| Some(u16::from_ne_bytes(c.try_into().ok()?)))
.collect();
String::from_utf16(&u16_bytes)
}
#[cfg(all(windows, feature = "wsl"))]
fn wsl_cmd(args: &[&str]) -> Result<String> {
use std::{os::windows::process::CommandExt, process::Command};
let output = Command::new("wsl")
.args(args)
.creation_flags(winapi::um::winbase::CREATE_NO_WINDOW)
.output()
.context("Failed to execute wsl")?;
process_utf16(&output.stdout).context("Failed to process stdout")
}
#[cfg(all(windows, feature = "wsl"))]
fn fetch_wsl2_distros() -> Vec<String> {
wsl_cmd(&["-l", "-q"])
.map(|stdout| {
stdout
.split('\n')
.filter(|s| !s.trim().is_empty())
.map(|s| s.trim().to_string())
.collect()
})
.unwrap_or_default()
}
pub fn config_ui(
ui: &mut egui::Ui,
state: &AppStateRef,
show_config_window: &mut bool,
config_state: &mut ConfigViewState,
appearance: &Appearance,
) {
let mut state_guard = state.write().unwrap();
let AppState {
config:
AppConfig {
project_dir, target_obj_dir, base_obj_dir, selected_obj, auto_update_check, ..
},
objects,
object_nodes,
..
} = &mut *state_guard;
ui.heading("Updates");
ui.checkbox(auto_update_check, "Check for updates on startup");
if ui.add_enabled(!config_state.check_update_running, egui::Button::new("Check now")).clicked()
{
config_state.queue_check_update = true;
}
ui.label(format!("Current version: {}", env!("CARGO_PKG_VERSION")));
if let Some(result) = &config_state.check_update {
ui.label(format!("Latest version: {}", result.latest_release.version));
if result.update_available {
ui.colored_label(appearance.insert_color, "Update available");
ui.horizontal(|ui| {
if let Some(bin_name) = &result.found_binary {
if ui
.add_enabled(!config_state.update_running, egui::Button::new("Automatic"))
.on_hover_text_at_pointer(
"Automatically download and replace the current build",
)
.clicked()
{
config_state.queue_update = Some(bin_name.clone());
}
}
if ui
.button("Manual")
.on_hover_text_at_pointer("Open a link to the latest release on GitHub")
.clicked()
{
ui.output_mut(|output| {
output.open_url =
Some(OpenUrl { url: RELEASE_URL.to_string(), new_tab: true })
});
}
});
}
}
ui.separator();
ui.horizontal(|ui| {
ui.heading("Project");
if ui.button(RichText::new("Settings")).clicked() {
*show_config_window = true;
}
});
let selected_index = selected_obj.as_ref().and_then(|selected_obj| {
objects.iter().position(|obj| obj.name.as_ref() == Some(&selected_obj.name))
});
let mut new_selected_index = selected_index;
if objects.is_empty() {
if let (Some(_base_dir), Some(target_dir)) = (base_obj_dir, target_obj_dir) {
if ui.button("Select object").clicked() {
config_state.file_dialog_state.queue(
|| {
Box::pin(
rfd::AsyncFileDialog::new()
.set_directory(target_dir)
.add_filter("Object file", &["o", "elf", "obj"])
.pick_file(),
)
},
FileDialogResult::Object,
);
}
if let Some(obj) = selected_obj {
ui.label(
RichText::new(&obj.name)
.color(appearance.replace_color)
.family(FontFamily::Monospace),
);
}
} else {
ui.colored_label(appearance.delete_color, "Missing project settings");
}
} else {
let had_search = !config_state.object_search.is_empty();
egui::TextEdit::singleline(&mut config_state.object_search).hint_text("Filter").ui(ui);
let mut root_open = None;
let mut node_open = NodeOpen::Default;
ui.horizontal(|ui| {
if ui.small_button("").on_hover_text_at_pointer("Collapse all").clicked() {
root_open = Some(false);
node_open = NodeOpen::Close;
}
if ui.small_button("").on_hover_text_at_pointer("Expand all").clicked() {
root_open = Some(true);
node_open = NodeOpen::Open;
}
if ui
.add_enabled(selected_obj.is_some(), egui::Button::new("").small())
.on_hover_text_at_pointer("Current object")
.clicked()
{
root_open = Some(true);
node_open = NodeOpen::Object;
}
let mut filters_text = RichText::new("Filter ⏷");
if config_state.filter_diffable
|| config_state.filter_incomplete
|| config_state.show_hidden
{
filters_text = filters_text.color(appearance.replace_color);
}
egui::menu::menu_button(ui, filters_text, |ui| {
ui.checkbox(&mut config_state.filter_diffable, "Diffable")
.on_hover_text_at_pointer("Only show objects with a source file");
ui.checkbox(&mut config_state.filter_incomplete, "Incomplete")
.on_hover_text_at_pointer("Only show objects not marked complete");
ui.checkbox(&mut config_state.show_hidden, "Hidden")
.on_hover_text_at_pointer("Show hidden (auto-generated) objects");
});
});
if config_state.object_search.is_empty() {
if had_search {
root_open = Some(true);
node_open = NodeOpen::Object;
}
} else if !had_search {
root_open = Some(true);
node_open = NodeOpen::Open;
}
CollapsingHeader::new(RichText::new("🗀 Objects").font(FontId {
size: appearance.ui_font.size,
family: appearance.code_font.family.clone(),
}))
.open(root_open)
.default_open(true)
.show(ui, |ui| {
let search = config_state.object_search.to_ascii_lowercase();
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
for node in object_nodes.iter().filter_map(|node| {
filter_node(
objects,
node,
&search,
config_state.filter_diffable,
config_state.filter_incomplete,
config_state.show_hidden,
)
}) {
display_node(
ui,
&mut new_selected_index,
project_dir.as_deref(),
objects,
&node,
appearance,
node_open,
);
}
});
}
if new_selected_index != selected_index {
if let Some(idx) = new_selected_index {
// Will set obj_changed, which will trigger a rebuild
let config = ObjectConfig::from(&objects[idx]);
state_guard.set_selected_obj(config);
}
}
if state_guard.config.selected_obj.is_some()
&& ui.add_enabled(!config_state.build_running, egui::Button::new("Build")).clicked()
{
config_state.queue_build = true;
}
}
fn display_unit(
ui: &mut egui::Ui,
selected_obj: &mut Option<usize>,
project_dir: Option<&Path>,
name: &str,
units: &[ProjectObject],
index: usize,
appearance: &Appearance,
) {
let object = &units[index];
let selected = *selected_obj == Some(index);
let color = if selected {
appearance.emphasized_text_color
} else if let Some(complete) = object.complete() {
if complete {
appearance.insert_color
} else {
appearance.delete_color
}
} else {
appearance.text_color
};
let response = SelectableLabel::new(
selected,
RichText::new(name)
.font(FontId {
size: appearance.ui_font.size,
family: appearance.code_font.family.clone(),
})
.color(color),
)
.ui(ui);
if get_source_path(project_dir, object).is_some() {
response.context_menu(|ui| object_context_ui(ui, object, project_dir));
}
if response.clicked() {
*selected_obj = Some(index);
}
}
fn get_source_path(project_dir: Option<&Path>, object: &ProjectObject) -> Option<PathBuf> {
project_dir.and_then(|dir| object.source_path().map(|path| dir.join(path)))
}
fn object_context_ui(ui: &mut egui::Ui, object: &ProjectObject, project_dir: Option<&Path>) {
if let Some(source_path) = get_source_path(project_dir, object) {
if ui
.button("Open source file")
.on_hover_text("Open the source file in the default editor")
.clicked()
{
log::info!("Opening file {}", source_path.display());
if let Err(e) = open::that_detached(&source_path) {
log::error!("Failed to open source file: {e}");
}
ui.close_menu();
}
}
}
#[derive(Default, Copy, Clone, PartialEq, Eq, Debug)]
enum NodeOpen {
#[default]
Default,
Open,
Close,
Object,
}
fn display_node(
ui: &mut egui::Ui,
selected_obj: &mut Option<usize>,
project_dir: Option<&Path>,
units: &[ProjectObject],
node: &ProjectObjectNode,
appearance: &Appearance,
node_open: NodeOpen,
) {
match node {
ProjectObjectNode::Unit(name, idx) => {
display_unit(ui, selected_obj, project_dir, name, units, *idx, appearance);
}
ProjectObjectNode::Dir(name, children) => {
let contains_obj = selected_obj.map(|idx| contains_node(node, idx));
let open = match node_open {
NodeOpen::Default => None,
NodeOpen::Open => Some(true),
NodeOpen::Close => Some(false),
NodeOpen::Object => contains_obj,
};
let color = if contains_obj == Some(true) {
appearance.replace_color
} else {
appearance.text_color
};
CollapsingHeader::new(
RichText::new(name)
.font(FontId {
size: appearance.ui_font.size,
family: appearance.code_font.family.clone(),
})
.color(color),
)
.open(open)
.show(ui, |ui| {
for node in children {
display_node(ui, selected_obj, project_dir, units, node, appearance, node_open);
}
});
}
}
}
fn contains_node(node: &ProjectObjectNode, selected_obj: usize) -> bool {
match node {
ProjectObjectNode::Unit(_, idx) => *idx == selected_obj,
ProjectObjectNode::Dir(_, children) => {
children.iter().any(|node| contains_node(node, selected_obj))
}
}
}
fn filter_node(
units: &[ProjectObject],
node: &ProjectObjectNode,
search: &str,
filter_diffable: bool,
filter_incomplete: bool,
show_hidden: bool,
) -> Option<ProjectObjectNode> {
match node {
ProjectObjectNode::Unit(name, idx) => {
let unit = &units[*idx];
if (search.is_empty() || name.to_ascii_lowercase().contains(search))
&& (!filter_diffable || (unit.base_path.is_some() && unit.target_path.is_some()))
&& (!filter_incomplete || matches!(unit.complete(), None | Some(false)))
&& (show_hidden || !unit.hidden())
{
Some(node.clone())
} else {
None
}
}
ProjectObjectNode::Dir(name, children) => {
let new_children = children
.iter()
.filter_map(|child| {
filter_node(
units,
child,
search,
filter_diffable,
filter_incomplete,
show_hidden,
)
})
.collect::<Vec<_>>();
if !new_children.is_empty() {
Some(ProjectObjectNode::Dir(name.clone(), new_children))
} else {
None
}
}
}
}
const HELP_ICON: &str = "";
fn subheading(ui: &mut egui::Ui, text: &str, appearance: &Appearance) {
ui.label(
RichText::new(text).size(appearance.ui_font.size).color(appearance.emphasized_text_color),
);
}
fn format_path(path: &Option<PathBuf>, appearance: &Appearance) -> RichText {
let mut color = appearance.replace_color;
let text = if let Some(dir) = path {
if let Some(rel) = dirs::home_dir().and_then(|home| dir.strip_prefix(&home).ok()) {
format!("~{}{}", MAIN_SEPARATOR, rel.display())
} else {
format!("{}", dir.display())
}
} else {
color = appearance.delete_color;
"[none]".to_string()
};
RichText::new(text).color(color).family(FontFamily::Monospace)
}
pub const CONFIG_DISABLED_TEXT: &str =
"Option disabled because it's set by the project configuration file.";
fn pick_folder_ui(
ui: &mut egui::Ui,
dir: &Option<PathBuf>,
label: &str,
tooltip: impl FnOnce(&mut egui::Ui),
appearance: &Appearance,
enabled: bool,
) -> egui::Response {
let response = ui.horizontal(|ui| {
subheading(ui, label, appearance);
ui.link(HELP_ICON).on_hover_ui(tooltip);
ui.add_enabled(enabled, egui::Button::new("Select"))
.on_disabled_hover_text(CONFIG_DISABLED_TEXT)
});
ui.label(format_path(dir, appearance));
response.inner
}
pub fn project_window(
ctx: &egui::Context,
state: &AppStateRef,
show: &mut bool,
config_state: &mut ConfigViewState,
appearance: &Appearance,
) {
let mut state_guard = state.write().unwrap();
egui::Window::new("Project").open(show).show(ctx, |ui| {
split_obj_config_ui(ui, &mut state_guard, config_state, appearance);
});
if let Some(error) = &state_guard.config_error {
let mut open = true;
egui::Window::new("Error").open(&mut open).show(ctx, |ui| {
ui.label("Failed to load project config:");
ui.colored_label(appearance.delete_color, error);
});
if !open {
state_guard.config_error = None;
}
}
}
fn split_obj_config_ui(
ui: &mut egui::Ui,
state: &mut AppState,
config_state: &mut ConfigViewState,
appearance: &Appearance,
) {
let text_format = TextFormat::simple(appearance.ui_font.clone(), appearance.text_color);
let code_format = TextFormat::simple(
FontId { size: appearance.ui_font.size, family: appearance.code_font.family.clone() },
appearance.emphasized_text_color,
);
let response = pick_folder_ui(
ui,
&state.config.project_dir,
"Project directory",
|ui| {
let mut job = LayoutJob::default();
job.append("The root project directory.\n\n", 0.0, text_format.clone());
job.append(
"If a configuration file exists, it will be loaded automatically.",
0.0,
text_format.clone(),
);
ui.label(job);
},
appearance,
true,
);
if response.clicked() {
config_state.file_dialog_state.queue(
|| Box::pin(rfd::AsyncFileDialog::new().pick_folder()),
FileDialogResult::ProjectDir,
);
}
ui.separator();
ui.horizontal(|ui| {
subheading(ui, "Build program", appearance);
ui.link(HELP_ICON).on_hover_ui(|ui| {
let mut job = LayoutJob::default();
job.append("By default, objdiff will build with ", 0.0, text_format.clone());
job.append("make", 0.0, code_format.clone());
job.append(
".\nIf the project uses a different build system (e.g. ",
0.0,
text_format.clone(),
);
job.append("ninja", 0.0, code_format.clone());
job.append(
"), specify it here.\nThe program must be in your ",
0.0,
text_format.clone(),
);
job.append("PATH", 0.0, code_format.clone());
job.append(".", 0.0, text_format.clone());
ui.label(job);
});
});
let mut custom_make_str = state.config.custom_make.clone().unwrap_or_default();
if ui
.add_enabled(
state.project_config_info.is_none(),
egui::TextEdit::singleline(&mut custom_make_str).hint_text("make"),
)
.on_disabled_hover_text(CONFIG_DISABLED_TEXT)
.changed()
{
if custom_make_str.is_empty() {
state.config.custom_make = None;
} else {
state.config.custom_make = Some(custom_make_str);
}
}
#[cfg(all(windows, feature = "wsl"))]
{
if config_state.available_wsl_distros.is_none() {
config_state.available_wsl_distros = Some(fetch_wsl2_distros());
}
egui::ComboBox::from_label("Run in WSL2")
.selected_text(
state.config.selected_wsl_distro.as_ref().unwrap_or(&"Disabled".to_string()),
)
.show_ui(ui, |ui| {
ui.selectable_value(&mut state.config.selected_wsl_distro, None, "Disabled");
for distro in config_state.available_wsl_distros.as_ref().unwrap() {
ui.selectable_value(
&mut state.config.selected_wsl_distro,
Some(distro.clone()),
distro,
);
}
});
}
ui.separator();
if let Some(project_dir) = state.config.project_dir.clone() {
let response = pick_folder_ui(
ui,
&state.config.target_obj_dir,
"Target build directory",
|ui| {
let mut job = LayoutJob::default();
job.append(
"This contains the \"target\" or \"expected\" objects, which are the intended result of the match.\n\n",
0.0,
text_format.clone(),
);
job.append(
"These are usually created by the project's build system or assembled.",
0.0,
text_format.clone(),
);
ui.label(job);
},
appearance,
state.project_config_info.is_none(),
);
if response.clicked() {
config_state.file_dialog_state.queue(
|| Box::pin(rfd::AsyncFileDialog::new().set_directory(&project_dir).pick_folder()),
FileDialogResult::TargetDir,
);
}
ui.add_enabled(
state.project_config_info.is_none(),
egui::Checkbox::new(&mut state.config.build_target, "Build target objects"),
)
.on_disabled_hover_text(CONFIG_DISABLED_TEXT)
.on_hover_ui(|ui| {
let mut job = LayoutJob::default();
job.append(
"Tells the build system to produce the target object.\n",
0.0,
text_format.clone(),
);
job.append("For example, this would call ", 0.0, text_format.clone());
job.append("make path/to/target.o", 0.0, code_format.clone());
job.append(".\n\n", 0.0, text_format.clone());
job.append(
"This is useful if the target objects are not already built\n",
0.0,
text_format.clone(),
);
job.append(
"or if they can change based on project configuration,\n",
0.0,
text_format.clone(),
);
job.append(
"but requires that the build system is configured correctly.",
0.0,
text_format.clone(),
);
ui.label(job);
});
ui.separator();
let response = pick_folder_ui(
ui,
&state.config.base_obj_dir,
"Base build directory",
|ui| {
let mut job = LayoutJob::default();
job.append(
"This contains the objects built from your decompiled code.",
0.0,
text_format.clone(),
);
ui.label(job);
},
appearance,
state.project_config_info.is_none(),
);
if response.clicked() {
config_state.file_dialog_state.queue(
|| Box::pin(rfd::AsyncFileDialog::new().set_directory(&project_dir).pick_folder()),
FileDialogResult::BaseDir,
);
}
ui.add_enabled(
state.project_config_info.is_none(),
egui::Checkbox::new(&mut state.config.build_base, "Build base objects"),
)
.on_disabled_hover_text(CONFIG_DISABLED_TEXT)
.on_hover_ui(|ui| {
let mut job = LayoutJob::default();
job.append(
"Tells the build system to produce the base object.\n",
0.0,
text_format.clone(),
);
job.append("For example, this would call ", 0.0, text_format.clone());
job.append("make path/to/base.o", 0.0, code_format.clone());
job.append(".\n\n", 0.0, text_format.clone());
job.append(
"This can be disabled if you're running the build system\n",
0.0,
text_format.clone(),
);
job.append(
"externally, and just want objdiff to reload the files\n",
0.0,
text_format.clone(),
);
job.append("when they change.", 0.0, text_format.clone());
ui.label(job);
});
ui.separator();
}
subheading(ui, "Watch settings", appearance);
let response =
ui.checkbox(&mut state.config.rebuild_on_changes, "Rebuild on changes").on_hover_ui(|ui| {
let mut job = LayoutJob::default();
job.append(
"Automatically re-run the build & diff when files change.",
0.0,
text_format.clone(),
);
ui.label(job);
});
if response.changed() {
state.watcher_change = true;
};
ui.horizontal(|ui| {
ui.label(RichText::new("File patterns").color(appearance.text_color));
if ui
.add_enabled(state.project_config_info.is_none(), egui::Button::new("Reset"))
.on_disabled_hover_text(CONFIG_DISABLED_TEXT)
.clicked()
{
state.config.watch_patterns =
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect();
state.watcher_change = true;
}
});
let mut remove_at: Option<usize> = None;
for (idx, glob) in state.config.watch_patterns.iter().enumerate() {
ui.horizontal(|ui| {
ui.label(
RichText::new(format!("{}", glob))
.color(appearance.text_color)
.family(FontFamily::Monospace),
);
if ui
.add_enabled(state.project_config_info.is_none(), egui::Button::new("-").small())
.on_disabled_hover_text(CONFIG_DISABLED_TEXT)
.clicked()
{
remove_at = Some(idx);
}
});
}
if let Some(idx) = remove_at {
state.config.watch_patterns.remove(idx);
state.watcher_change = true;
}
ui.horizontal(|ui| {
ui.add_enabled(
state.project_config_info.is_none(),
egui::TextEdit::singleline(&mut config_state.watch_pattern_text).desired_width(100.0),
)
.on_disabled_hover_text(CONFIG_DISABLED_TEXT);
if ui
.add_enabled(state.project_config_info.is_none(), egui::Button::new("+").small())
.on_disabled_hover_text(CONFIG_DISABLED_TEXT)
.clicked()
{
if let Ok(glob) = Glob::new(&config_state.watch_pattern_text) {
state.config.watch_patterns.push(glob);
state.watcher_change = true;
config_state.watch_pattern_text.clear();
}
}
});
}
pub fn arch_config_window(
ctx: &egui::Context,
state: &AppStateRef,
show: &mut bool,
appearance: &Appearance,
) {
let mut state_guard = state.write().unwrap();
egui::Window::new("Arch Settings").open(show).show(ctx, |ui| {
arch_config_ui(ui, &mut state_guard, appearance);
});
}
fn arch_config_ui(ui: &mut egui::Ui, state: &mut AppState, _appearance: &Appearance) {
ui.heading("x86");
egui::ComboBox::new("x86_formatter", "Format")
.selected_text(state.config.diff_obj_config.x86_formatter.get_message().unwrap())
.show_ui(ui, |ui| {
for &formatter in X86Formatter::VARIANTS {
if ui
.selectable_label(
state.config.diff_obj_config.x86_formatter == formatter,
formatter.get_message().unwrap(),
)
.clicked()
{
state.config.diff_obj_config.x86_formatter = formatter;
state.queue_reload = true;
}
}
});
ui.separator();
ui.heading("MIPS");
egui::ComboBox::new("mips_abi", "ABI")
.selected_text(state.config.diff_obj_config.mips_abi.get_message().unwrap())
.show_ui(ui, |ui| {
for &abi in MipsAbi::VARIANTS {
if ui
.selectable_label(
state.config.diff_obj_config.mips_abi == abi,
abi.get_message().unwrap(),
)
.clicked()
{
state.config.diff_obj_config.mips_abi = abi;
state.queue_reload = true;
}
}
});
egui::ComboBox::new("mips_instr_category", "Instruction Category")
.selected_text(state.config.diff_obj_config.mips_instr_category.get_message().unwrap())
.show_ui(ui, |ui| {
for &category in MipsInstrCategory::VARIANTS {
if ui
.selectable_label(
state.config.diff_obj_config.mips_instr_category == category,
category.get_message().unwrap(),
)
.clicked()
{
state.config.diff_obj_config.mips_instr_category = category;
state.queue_reload = true;
}
}
});
ui.separator();
ui.heading("ARM");
egui::ComboBox::new("arm_arch_version", "Architecture Version")
.selected_text(state.config.diff_obj_config.arm_arch_version.get_message().unwrap())
.show_ui(ui, |ui| {
for &version in ArmArchVersion::VARIANTS {
if ui
.selectable_label(
state.config.diff_obj_config.arm_arch_version == version,
version.get_message().unwrap(),
)
.clicked()
{
state.config.diff_obj_config.arm_arch_version = version;
state.queue_reload = true;
}
}
});
let response = ui
.checkbox(&mut state.config.diff_obj_config.arm_unified_syntax, "Unified syntax")
.on_hover_text("Disassemble as unified assembly language (UAL).");
if response.changed() {
state.queue_reload = true;
}
let response = ui
.checkbox(&mut state.config.diff_obj_config.arm_av_registers, "Use A/V registers")
.on_hover_text("Display R0-R3 as A1-A4 and R4-R11 as V1-V8");
if response.changed() {
state.queue_reload = true;
}
egui::ComboBox::new("arm_r9_usage", "Display R9 as")
.selected_text(state.config.diff_obj_config.arm_r9_usage.get_message().unwrap())
.show_ui(ui, |ui| {
for &usage in ArmR9Usage::VARIANTS {
if ui
.selectable_label(
state.config.diff_obj_config.arm_r9_usage == usage,
usage.get_message().unwrap(),
)
.on_hover_text(usage.get_detailed_message().unwrap())
.clicked()
{
state.config.diff_obj_config.arm_r9_usage = usage;
state.queue_reload = true;
}
}
});
let response = ui
.checkbox(&mut state.config.diff_obj_config.arm_sl_usage, "Display R10 as SL")
.on_hover_text("Used for explicit stack limits.");
if response.changed() {
state.queue_reload = true;
}
let response = ui
.checkbox(&mut state.config.diff_obj_config.arm_fp_usage, "Display R11 as FP")
.on_hover_text("Used for frame pointers.");
if response.changed() {
state.queue_reload = true;
}
let response = ui
.checkbox(&mut state.config.diff_obj_config.arm_ip_usage, "Display R12 as IP")
.on_hover_text("Used for interworking and long branches.");
if response.changed() {
state.queue_reload = true;
}
}

View File

@@ -0,0 +1,292 @@
use std::{cmp::min, default::Default, mem::take};
use egui::{text::LayoutJob, Id, Label, RichText, Sense, Widget};
use objdiff_core::{
diff::{ObjDataDiff, ObjDataDiffKind, ObjDiff},
obj::ObjInfo,
};
use time::format_description;
use crate::views::{
appearance::Appearance,
column_layout::{render_header, render_table},
symbol_diff::{DiffViewAction, DiffViewNavigation, DiffViewState},
write_text,
};
const BYTES_PER_ROW: usize = 16;
fn find_section(obj: &ObjInfo, section_name: &str) -> Option<usize> {
obj.sections.iter().position(|section| section.name == section_name)
}
fn data_row_ui(ui: &mut egui::Ui, address: usize, diffs: &[ObjDataDiff], appearance: &Appearance) {
if diffs.iter().any(|d| d.kind != ObjDataDiffKind::None) {
ui.painter().rect_filled(ui.available_rect_before_wrap(), 0.0, ui.visuals().faint_bg_color);
}
let mut job = LayoutJob::default();
write_text(
format!("{address:08x}: ").as_str(),
appearance.text_color,
&mut job,
appearance.code_font.clone(),
);
let mut cur_addr = 0usize;
for diff in diffs {
let base_color = match diff.kind {
ObjDataDiffKind::None => appearance.text_color,
ObjDataDiffKind::Replace => appearance.replace_color,
ObjDataDiffKind::Delete => appearance.delete_color,
ObjDataDiffKind::Insert => appearance.insert_color,
};
if diff.data.is_empty() {
let mut str = " ".repeat(diff.len);
str.push_str(" ".repeat(diff.len / 8).as_str());
write_text(str.as_str(), base_color, &mut job, appearance.code_font.clone());
cur_addr += diff.len;
} else {
let mut text = String::new();
for byte in &diff.data {
text.push_str(format!("{byte:02x} ").as_str());
cur_addr += 1;
if cur_addr % 8 == 0 {
text.push(' ');
}
}
write_text(text.as_str(), base_color, &mut job, appearance.code_font.clone());
}
}
if cur_addr < BYTES_PER_ROW {
let n = BYTES_PER_ROW - cur_addr;
let mut str = " ".to_string();
str.push_str(" ".repeat(n).as_str());
str.push_str(" ".repeat(n / 8).as_str());
write_text(str.as_str(), appearance.text_color, &mut job, appearance.code_font.clone());
}
write_text(" ", appearance.text_color, &mut job, appearance.code_font.clone());
for diff in diffs {
let base_color = match diff.kind {
ObjDataDiffKind::None => appearance.text_color,
ObjDataDiffKind::Replace => appearance.replace_color,
ObjDataDiffKind::Delete => appearance.delete_color,
ObjDataDiffKind::Insert => appearance.insert_color,
};
if diff.data.is_empty() {
write_text(
" ".repeat(diff.len).as_str(),
base_color,
&mut job,
appearance.code_font.clone(),
);
} else {
let mut text = String::new();
for byte in &diff.data {
let c = char::from(*byte);
if c.is_ascii() && !c.is_ascii_control() {
text.push(c);
} else {
text.push('.');
}
}
write_text(text.as_str(), base_color, &mut job, appearance.code_font.clone());
}
}
Label::new(job).sense(Sense::click()).ui(ui);
// .on_hover_ui_at_pointer(|ui| ins_hover_ui(ui, ins))
// .context_menu(|ui| ins_context_menu(ui, ins));
}
fn split_diffs(diffs: &[ObjDataDiff]) -> Vec<Vec<ObjDataDiff>> {
let mut split_diffs = Vec::<Vec<ObjDataDiff>>::new();
let mut row_diffs = Vec::<ObjDataDiff>::new();
let mut cur_addr = 0usize;
for diff in diffs {
let mut cur_len = 0usize;
while cur_len < diff.len {
let remaining_len = diff.len - cur_len;
let mut remaining_in_row = BYTES_PER_ROW - (cur_addr % BYTES_PER_ROW);
let len = min(remaining_len, remaining_in_row);
row_diffs.push(ObjDataDiff {
data: if diff.data.is_empty() {
Vec::new()
} else {
diff.data[cur_len..cur_len + len].to_vec()
},
kind: diff.kind,
len,
// TODO
symbol: String::new(),
});
remaining_in_row -= len;
cur_len += len;
cur_addr += len;
if remaining_in_row == 0 {
split_diffs.push(take(&mut row_diffs));
}
}
}
if !row_diffs.is_empty() {
split_diffs.push(take(&mut row_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(
ui: &mut egui::Ui,
available_width: f32,
left_ctx: Option<SectionDiffContext<'_>>,
right_ctx: Option<SectionDiffContext<'_>>,
config: &Appearance,
) -> Option<()> {
let left_section = left_ctx
.and_then(|ctx| ctx.section_index.map(|i| (&ctx.obj.sections[i], &ctx.diff.sections[i])));
let right_section = right_ctx
.and_then(|ctx| ctx.section_index.map(|i| (&ctx.obj.sections[i], &ctx.diff.sections[i])));
let total_bytes = left_section
.or(right_section)?
.1
.data_diff
.iter()
.fold(0usize, |accum, item| accum + item.len);
if total_bytes == 0 {
return None;
}
let total_rows = (total_bytes - 1) / BYTES_PER_ROW + 1;
let left_diffs = left_section.map(|(_, section)| split_diffs(&section.data_diff));
let right_diffs = right_section.map(|(_, section)| split_diffs(&section.data_diff));
render_table(ui, available_width, 2, config.code_font.size, total_rows, |row, column| {
let i = row.index();
let address = i * BYTES_PER_ROW;
row.col(|ui| {
if column == 0 {
if let Some(left_diffs) = &left_diffs {
data_row_ui(ui, address, &left_diffs[i], config);
}
} else if column == 1 {
if let Some(right_diffs) = &right_diffs {
data_row_ui(ui, address, &right_diffs[i], config);
}
}
});
});
Some(())
}
#[must_use]
pub fn data_diff_ui(
ui: &mut egui::Ui,
state: &DiffViewState,
appearance: &Appearance,
) -> Option<DiffViewAction> {
let mut ret = None;
let Some(result) = &state.build else {
return ret;
};
let section_name =
state.symbol_state.left_symbol.as_ref().and_then(|s| s.section_name.as_deref()).or_else(
|| state.symbol_state.right_symbol.as_ref().and_then(|s| s.section_name.as_deref()),
);
let left_ctx = SectionDiffContext::new(result.first_obj.as_ref(), section_name);
let right_ctx = SectionDiffContext::new(result.second_obj.as_ref(), section_name);
// If both sides are missing a symbol, switch to symbol diff view
if !right_ctx.is_some_and(|ctx| ctx.has_section())
&& !left_ctx.is_some_and(|ctx| ctx.has_section())
{
return Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
}
// Header
let available_width = ui.available_width();
render_header(ui, available_width, 2, |ui, column| {
if column == 0 {
// Left column
if ui.button("⏴ Back").clicked() {
ret = Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
}
if let Some(section) =
left_ctx.and_then(|ctx| ctx.section_index.map(|i| &ctx.obj.sections[i]))
{
ui.label(
RichText::new(section.name.clone())
.font(appearance.code_font.clone())
.color(appearance.highlight_color),
);
} else {
ui.label(
RichText::new("Missing")
.font(appearance.code_font.clone())
.color(appearance.replace_color),
);
}
} else if column == 1 {
// Right column
ui.horizontal(|ui| {
if ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() {
ret = Some(DiffViewAction::Build);
}
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
if state.build_running {
ui.colored_label(appearance.replace_color, "Building…");
} else {
ui.label("Last built:");
let format = format_description::parse("[hour]:[minute]:[second]").unwrap();
ui.label(
result.time.to_offset(appearance.utc_offset).format(&format).unwrap(),
);
}
});
});
if let Some(section) =
right_ctx.and_then(|ctx| ctx.section_index.map(|i| &ctx.obj.sections[i]))
{
ui.label(
RichText::new(section.name.clone())
.font(appearance.code_font.clone())
.color(appearance.highlight_color),
);
} else {
ui.label(
RichText::new("Missing")
.font(appearance.code_font.clone())
.color(appearance.replace_color),
);
}
}
});
// Table
let id =
Id::new(state.symbol_state.left_symbol.as_ref().and_then(|s| s.section_name.as_deref()))
.with(state.symbol_state.right_symbol.as_ref().and_then(|s| s.section_name.as_deref()));
ui.push_id(id, |ui| {
data_table_ui(ui, available_width, left_ctx, right_ctx, appearance);
});
ret
}

View File

@@ -0,0 +1,20 @@
use crate::views::{appearance::Appearance, frame_history::FrameHistory};
pub fn debug_window(
ctx: &egui::Context,
show: &mut bool,
frame_history: &mut FrameHistory,
appearance: &Appearance,
) {
egui::Window::new("Debug").open(show).show(ctx, |ui| {
debug_ui(ui, frame_history, appearance);
});
}
fn debug_ui(ui: &mut egui::Ui, frame_history: &mut FrameHistory, _appearance: &Appearance) {
if ui.button("Clear memory").clicked() {
ui.memory_mut(|m| *m = Default::default());
}
ui.label(format!("Repainting the UI each frame. FPS: {:.1}", frame_history.fps()));
frame_history.ui(ui);
}

View File

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

View File

@@ -0,0 +1,248 @@
use egui::{RichText, ScrollArea};
use objdiff_core::{
arch::ppc::ExceptionInfo,
obj::{ObjInfo, ObjSymbol},
};
use time::format_description;
use crate::views::{
appearance::Appearance,
column_layout::{render_header, render_strips},
function_diff::FunctionDiffContext,
symbol_diff::{
match_color_for_symbol, DiffViewAction, DiffViewNavigation, DiffViewState, SymbolRefByName,
View,
},
};
fn decode_extab(extab: &ExceptionInfo) -> String {
let mut text = String::from("");
let mut dtor_names: Vec<String> = vec![];
for dtor in &extab.dtors {
//For each function name, use the demangled name by default,
//and if not available fallback to the original name
let name: String = match &dtor.demangled_name {
Some(demangled_name) => demangled_name.to_string(),
None => dtor.name.clone(),
};
dtor_names.push(name);
}
if let Some(decoded) = extab.data.to_string(dtor_names) {
text += decoded.as_str();
}
text
}
fn find_extab_entry<'a>(obj: &'a ObjInfo, symbol: &ObjSymbol) -> Option<&'a ExceptionInfo> {
obj.arch.ppc().and_then(|ppc| ppc.extab_for_symbol(symbol))
}
fn extab_text_ui(
ui: &mut egui::Ui,
ctx: FunctionDiffContext<'_>,
symbol: &ObjSymbol,
appearance: &Appearance,
) -> Option<()> {
if let Some(extab_entry) = find_extab_entry(ctx.obj, symbol) {
let text = decode_extab(extab_entry);
ui.colored_label(appearance.replace_color, &text);
return Some(());
}
None
}
fn extab_ui(
ui: &mut egui::Ui,
ctx: FunctionDiffContext<'_>,
appearance: &Appearance,
_column: usize,
) {
ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| {
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
if let Some((_section, symbol)) =
ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref))
{
extab_text_ui(ui, ctx, symbol, appearance);
}
});
});
}
#[must_use]
pub fn extab_diff_ui(
ui: &mut egui::Ui,
state: &DiffViewState,
appearance: &Appearance,
) -> Option<DiffViewAction> {
let mut ret = None;
let Some(result) = &state.build else {
return ret;
};
let mut left_ctx = FunctionDiffContext::new(
result.first_obj.as_ref(),
state.symbol_state.left_symbol.as_ref(),
);
let mut right_ctx = FunctionDiffContext::new(
result.second_obj.as_ref(),
state.symbol_state.right_symbol.as_ref(),
);
// If one side is missing a symbol, but the diff process found a match, use that symbol
let left_diff_symbol = left_ctx.and_then(|ctx| {
ctx.symbol_ref.and_then(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).target_symbol)
});
let right_diff_symbol = right_ctx.and_then(|ctx| {
ctx.symbol_ref.and_then(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).target_symbol)
});
if left_diff_symbol.is_some() && right_ctx.is_some_and(|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.is_some_and(|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.is_some_and(|ctx| !ctx.has_symbol())
&& left_ctx.is_some_and(|ctx| !ctx.has_symbol())
{
return Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
}
// Header
let available_width = ui.available_width();
render_header(ui, available_width, 2, |ui, column| {
if column == 0 {
// Left column
ui.horizontal(|ui| {
if ui.button("⏴ Back").clicked() {
ret = Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
}
ui.separator();
if ui
.add_enabled(
!state.scratch_running
&& state.scratch_available
&& left_ctx.is_some_and(|ctx| ctx.has_symbol()),
egui::Button::new("📲 decomp.me"),
)
.on_hover_text_at_pointer("Create a new scratch on decomp.me (beta)")
.on_disabled_hover_text("Scratch configuration missing")
.clicked()
{
if let Some((_section, symbol)) = left_ctx.and_then(|ctx| {
ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref))
}) {
ret = Some(DiffViewAction::CreateScratch(symbol.name.clone()));
}
}
});
if let Some((_section, symbol)) = left_ctx
.and_then(|ctx| ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref)))
{
let name = symbol.demangled_name.as_deref().unwrap_or(&symbol.name);
ui.label(
RichText::new(name)
.font(appearance.code_font.clone())
.color(appearance.highlight_color),
);
} else {
ui.label(
RichText::new("Missing")
.font(appearance.code_font.clone())
.color(appearance.replace_color),
);
}
} else if column == 1 {
// Right column
ui.horizontal(|ui| {
if ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() {
ret = Some(DiffViewAction::Build);
}
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
if state.build_running {
ui.colored_label(appearance.replace_color, "Building…");
} else {
ui.label("Last built:");
let format = format_description::parse("[hour]:[minute]:[second]").unwrap();
ui.label(
result.time.to_offset(appearance.utc_offset).format(&format).unwrap(),
);
}
});
ui.separator();
if ui
.add_enabled(state.source_path_available, egui::Button::new("🖹 Source file"))
.on_hover_text_at_pointer("Open the source file in the default editor")
.on_disabled_hover_text("Source file metadata missing")
.clicked()
{
ret = Some(DiffViewAction::OpenSourcePath);
}
});
if let Some(((_section, symbol), symbol_diff)) = right_ctx.and_then(|ctx| {
ctx.symbol_ref.map(|symbol_ref| {
(ctx.obj.section_symbol(symbol_ref), ctx.diff.symbol_diff(symbol_ref))
})
}) {
let name = symbol.demangled_name.as_deref().unwrap_or(&symbol.name);
ui.label(
RichText::new(name)
.font(appearance.code_font.clone())
.color(appearance.highlight_color),
);
if let Some(match_percent) = symbol_diff.match_percent {
ui.label(
RichText::new(format!("{:.0}%", match_percent.floor()))
.font(appearance.code_font.clone())
.color(match_color_for_symbol(match_percent, appearance)),
);
}
} else {
ui.label(
RichText::new("Missing")
.font(appearance.code_font.clone())
.color(appearance.replace_color),
);
}
}
});
// Table
render_strips(ui, available_width, 2, |ui, column| {
if column == 0 {
if let Some(ctx) = left_ctx {
extab_ui(ui, ctx, appearance, column);
}
} else if column == 1 {
if let Some(ctx) = right_ctx {
extab_ui(ui, ctx, appearance, column);
}
}
});
ret
}

View File

@@ -0,0 +1,51 @@
use std::{future::Future, path::PathBuf, pin::Pin, thread::JoinHandle};
use pollster::FutureExt;
use rfd::FileHandle;
#[derive(Default)]
pub enum FileDialogResult {
#[default]
None,
ProjectDir(PathBuf),
TargetDir(PathBuf),
BaseDir(PathBuf),
Object(PathBuf),
}
#[derive(Default)]
pub struct FileDialogState {
thread: Option<JoinHandle<FileDialogResult>>,
}
impl FileDialogState {
pub fn queue<InitCb, ResultCb>(&mut self, init: InitCb, result_cb: ResultCb)
where
InitCb: FnOnce() -> Pin<Box<dyn Future<Output = Option<FileHandle>> + Send>>,
ResultCb: FnOnce(PathBuf) -> FileDialogResult + Send + 'static,
{
if self.thread.is_some() {
return;
}
let future = init();
self.thread = Some(std::thread::spawn(move || {
if let Some(handle) = future.block_on() {
result_cb(PathBuf::from(handle))
} else {
FileDialogResult::None
}
}));
}
pub fn poll(&mut self) -> FileDialogResult {
if let Some(thread) = &mut self.thread {
if thread.is_finished() {
self.thread.take().unwrap().join().unwrap_or(FileDialogResult::None)
} else {
FileDialogResult::None
}
} else {
FileDialogResult::None
}
}
}

View File

@@ -0,0 +1,142 @@
// From https://github.com/emilk/egui/blob/e037489ac20a9e419715ae75d205a8baa117c3cf/crates/egui_demo_app/src/frame_history.rs
// Copyright (c) 2018-2021 Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
//
// Permission is hereby granted, free of charge, to any
// person obtaining a copy of this software and associated
// documentation files (the "Software"), to deal in the
// Software without restriction, including without
// limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software
// is furnished to do so, subject to the following
// conditions:
//
// The above copyright notice and this permission notice
// shall be included in all copies or substantial portions
// of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
// ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
// SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
use egui::util::History;
pub struct FrameHistory {
frame_times: History<f32>,
}
impl Default for FrameHistory {
fn default() -> Self {
let max_age: f32 = 1.0;
let max_len = (max_age * 300.0).round() as usize;
Self { frame_times: History::new(0..max_len, max_age) }
}
}
impl FrameHistory {
// Called first
pub fn on_new_frame(&mut self, now: f64, previous_frame_time: Option<f32>) {
let previous_frame_time = previous_frame_time.unwrap_or_default();
if let Some(latest) = self.frame_times.latest_mut() {
*latest = previous_frame_time; // rewrite history now that we know
}
self.frame_times.add(now, previous_frame_time); // projected
}
pub fn mean_frame_time(&self) -> f32 { self.frame_times.average().unwrap_or_default() }
pub fn fps(&self) -> f32 { 1.0 / self.frame_times.mean_time_interval().unwrap_or_default() }
pub fn ui(&mut self, ui: &mut egui::Ui) {
ui.label(format!("Mean CPU usage: {:.2} ms / frame", 1e3 * self.mean_frame_time()))
.on_hover_text(
"Includes egui layout and tessellation time.\n\
Does not include GPU usage, nor overhead for sending data to GPU.",
);
egui::warn_if_debug_build(ui);
if !cfg!(target_arch = "wasm32") {
egui::CollapsingHeader::new("📊 CPU usage history").default_open(false).show(
ui,
|ui| {
self.graph(ui);
},
);
}
}
fn graph(&mut self, ui: &mut egui::Ui) -> egui::Response {
use egui::*;
ui.label("egui CPU usage history");
let history = &self.frame_times;
// TODO(emilk): we should not use `slider_width` as default graph width.
let height = ui.spacing().slider_width;
let size = vec2(ui.available_size_before_wrap().x, height);
let (rect, response) = ui.allocate_at_least(size, Sense::hover());
let style = ui.style().noninteractive();
let graph_top_cpu_usage = 0.010;
let graph_rect = Rect::from_x_y_ranges(history.max_age()..=0.0, graph_top_cpu_usage..=0.0);
let to_screen = emath::RectTransform::from_to(graph_rect, rect);
let mut shapes = Vec::with_capacity(3 + 2 * history.len());
shapes.push(Shape::Rect(epaint::RectShape::new(
rect,
style.rounding,
ui.visuals().extreme_bg_color,
ui.style().noninteractive().bg_stroke,
)));
let rect = rect.shrink(4.0);
let color = ui.visuals().text_color();
let line_stroke = Stroke::new(1.0, color);
if let Some(pointer_pos) = response.hover_pos() {
let y = pointer_pos.y;
shapes.push(Shape::line_segment(
[pos2(rect.left(), y), pos2(rect.right(), y)],
line_stroke,
));
let cpu_usage = to_screen.inverse().transform_pos(pointer_pos).y;
let text = format!("{:.1} ms", 1e3 * cpu_usage);
shapes.push(ui.fonts(|f| {
Shape::text(
f,
pos2(rect.left(), y),
egui::Align2::LEFT_BOTTOM,
text,
TextStyle::Monospace.resolve(ui.style()),
color,
)
}));
}
let circle_color = color;
let radius = 2.0;
let right_side_time = ui.input(|i| i.time); // Time at right side of screen
for (time, cpu_usage) in history.iter() {
let age = (right_side_time - time) as f32;
let pos = to_screen.transform_pos_clamped(Pos2::new(age, cpu_usage));
shapes.push(Shape::line_segment([pos2(pos.x, rect.bottom()), pos], line_stroke));
if cpu_usage < graph_top_cpu_usage {
shapes.push(Shape::circle_filled(pos, radius, circle_color));
}
}
ui.painter().extend(shapes);
response
}
}

View File

@@ -0,0 +1,834 @@
use std::default::Default;
use egui::{text::LayoutJob, Id, Label, Response, RichText, Sense, Widget};
use egui_extras::TableRow;
use objdiff_core::{
diff::{
display::{display_diff, DiffText, HighlightKind},
ObjDiff, ObjInsDiff, ObjInsDiffKind,
},
obj::{
ObjInfo, ObjIns, ObjInsArg, ObjInsArgValue, ObjSection, ObjSectionKind, ObjSymbol,
SymbolRef,
},
};
use time::format_description;
use crate::views::{
appearance::Appearance,
column_layout::{render_header, render_strips, render_table},
symbol_diff::{
match_color_for_symbol, symbol_list_ui, DiffViewAction, DiffViewNavigation, DiffViewState,
SymbolDiffContext, SymbolFilter, SymbolRefByName, SymbolViewState, View,
},
};
#[derive(Default)]
pub struct FunctionViewState {
left_highlight: HighlightKind,
right_highlight: HighlightKind,
}
impl FunctionViewState {
pub fn highlight(&self, column: usize) -> &HighlightKind {
match column {
0 => &self.left_highlight,
1 => &self.right_highlight,
_ => &HighlightKind::None,
}
}
pub fn set_highlight(&mut self, column: usize, highlight: HighlightKind) {
match column {
0 => {
if highlight == self.left_highlight {
if highlight == self.right_highlight {
self.left_highlight = HighlightKind::None;
self.right_highlight = HighlightKind::None;
} else {
self.right_highlight = self.left_highlight.clone();
}
} else {
self.left_highlight = highlight;
}
}
1 => {
if highlight == self.right_highlight {
if highlight == self.left_highlight {
self.left_highlight = HighlightKind::None;
self.right_highlight = HighlightKind::None;
} else {
self.left_highlight = self.right_highlight.clone();
}
} else {
self.right_highlight = highlight;
}
}
_ => {}
}
}
pub fn clear_highlight(&mut self) {
self.left_highlight = HighlightKind::None;
self.right_highlight = HighlightKind::None;
}
}
fn ins_hover_ui(
ui: &mut egui::Ui,
obj: &ObjInfo,
section: &ObjSection,
ins: &ObjIns,
symbol: &ObjSymbol,
appearance: &Appearance,
) {
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
let offset = ins.address - section.address;
ui.label(format!(
"{:02x?}",
&section.data[offset as usize..(offset + ins.size as u64) as usize]
));
if let Some(virtual_address) = symbol.virtual_address {
let offset = ins.address - symbol.address;
ui.colored_label(
appearance.replace_color,
format!("Virtual address: {:#x}", virtual_address + offset),
);
}
if let Some(orig) = &ins.orig {
ui.label(format!("Original: {}", orig));
}
for arg in &ins.args {
if let ObjInsArg::Arg(arg) = arg {
match arg {
ObjInsArgValue::Signed(v) => {
ui.label(format!("{arg} == {v}"));
}
ObjInsArgValue::Unsigned(v) => {
ui.label(format!("{arg} == {v}"));
}
_ => {}
}
}
}
if let Some(reloc) = &ins.reloc {
ui.label(format!("Relocation type: {}", obj.arch.display_reloc(reloc.flags)));
ui.colored_label(appearance.highlight_color, format!("Name: {}", reloc.target.name));
if let Some(orig_section_index) = reloc.target.orig_section_index {
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(
appearance.highlight_color,
format!("Address: {:x}", reloc.target.address),
);
ui.colored_label(
appearance.highlight_color,
format!("Size: {:x}", reloc.target.size),
);
if let Some(s) = obj
.arch
.guess_data_type(ins)
.and_then(|ty| obj.arch.display_data_type(ty, &reloc.target.bytes))
{
ui.colored_label(appearance.highlight_color, s);
}
} else {
ui.colored_label(appearance.highlight_color, "Extern".to_string());
}
}
if let Some(demangled) = rlwinmdec::decode(&ins.formatted) {
ui.colored_label(appearance.highlight_color, demangled.trim());
}
});
}
fn ins_context_menu(ui: &mut egui::Ui, section: &ObjSection, ins: &ObjIns, symbol: &ObjSymbol) {
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
if ui.button(format!("Copy \"{}\"", ins.formatted)).clicked() {
ui.output_mut(|output| output.copied_text.clone_from(&ins.formatted));
ui.close_menu();
}
let mut hex_string = "0x".to_string();
for byte in &section.data[ins.address as usize..(ins.address + ins.size as u64) as usize] {
hex_string.push_str(&format!("{:02x}", byte));
}
if ui.button(format!("Copy \"{hex_string}\" (instruction bytes)")).clicked() {
ui.output_mut(|output| output.copied_text = hex_string);
ui.close_menu();
}
if let Some(virtual_address) = symbol.virtual_address {
let offset = ins.address - symbol.address;
let offset_string = format!("{:#x}", virtual_address + offset);
if ui.button(format!("Copy \"{offset_string}\" (virtual address)")).clicked() {
ui.output_mut(|output| output.copied_text = offset_string);
ui.close_menu();
}
}
for arg in &ins.args {
if let ObjInsArg::Arg(arg) = arg {
match arg {
ObjInsArgValue::Signed(v) => {
if ui.button(format!("Copy \"{arg}\"")).clicked() {
ui.output_mut(|output| output.copied_text = arg.to_string());
ui.close_menu();
}
if ui.button(format!("Copy \"{v}\"")).clicked() {
ui.output_mut(|output| output.copied_text = v.to_string());
ui.close_menu();
}
}
ObjInsArgValue::Unsigned(v) => {
if ui.button(format!("Copy \"{arg}\"")).clicked() {
ui.output_mut(|output| output.copied_text = arg.to_string());
ui.close_menu();
}
if ui.button(format!("Copy \"{v}\"")).clicked() {
ui.output_mut(|output| output.copied_text = v.to_string());
ui.close_menu();
}
}
_ => {}
}
}
}
if let Some(reloc) = &ins.reloc {
if let Some(name) = &reloc.target.demangled_name {
if ui.button(format!("Copy \"{name}\"")).clicked() {
ui.output_mut(|output| output.copied_text.clone_from(name));
ui.close_menu();
}
}
if ui.button(format!("Copy \"{}\"", reloc.target.name)).clicked() {
ui.output_mut(|output| output.copied_text.clone_from(&reloc.target.name));
ui.close_menu();
}
}
});
}
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
}
#[must_use]
#[expect(clippy::too_many_arguments)]
fn diff_text_ui(
ui: &mut egui::Ui,
text: DiffText<'_>,
ins_diff: &ObjInsDiff,
appearance: &Appearance,
ins_view_state: &FunctionViewState,
column: usize,
space_width: f32,
response_cb: impl Fn(Response) -> Response,
) -> Option<DiffViewAction> {
let mut ret = None;
let label_text;
let mut base_color = match ins_diff.kind {
ObjInsDiffKind::None | ObjInsDiffKind::OpMismatch | ObjInsDiffKind::ArgMismatch => {
appearance.text_color
}
ObjInsDiffKind::Replace => appearance.replace_color,
ObjInsDiffKind::Delete => appearance.delete_color,
ObjInsDiffKind::Insert => appearance.insert_color,
};
let mut pad_to = 0;
match text {
DiffText::Basic(text) => {
label_text = text.to_string();
}
DiffText::BasicColor(s, idx) => {
label_text = s.to_string();
base_color = appearance.diff_colors[idx % appearance.diff_colors.len()];
}
DiffText::Line(num) => {
label_text = num.to_string();
base_color = appearance.deemphasized_text_color;
pad_to = 5;
}
DiffText::Address(addr) => {
label_text = format!("{:x}:", addr);
pad_to = 5;
}
DiffText::Opcode(mnemonic, _op) => {
label_text = mnemonic.to_string();
if ins_diff.kind == ObjInsDiffKind::OpMismatch {
base_color = appearance.replace_color;
}
pad_to = 8;
}
DiffText::Argument(arg, diff) => {
label_text = arg.to_string();
if let Some(diff) = diff {
base_color = appearance.diff_colors[diff.idx % appearance.diff_colors.len()]
}
}
DiffText::BranchDest(addr, diff) => {
label_text = format!("{addr:x}");
if let Some(diff) = diff {
base_color = appearance.diff_colors[diff.idx % appearance.diff_colors.len()]
}
}
DiffText::Symbol(sym, diff) => {
let name = sym.demangled_name.as_ref().unwrap_or(&sym.name);
label_text = name.clone();
if let Some(diff) = diff {
base_color = appearance.diff_colors[diff.idx % appearance.diff_colors.len()]
} else {
base_color = appearance.emphasized_text_color;
}
}
DiffText::Spacing(n) => {
ui.add_space(n as f32 * space_width);
return ret;
}
DiffText::Eol => {
label_text = "\n".to_string();
}
}
let len = label_text.len();
let highlight = *ins_view_state.highlight(column) == text;
let mut response = Label::new(LayoutJob::single_section(
label_text,
appearance.code_text_format(base_color, highlight),
))
.sense(Sense::click())
.ui(ui);
response = response_cb(response);
if response.clicked() {
ret = Some(DiffViewAction::SetDiffHighlight(column, text.into()));
}
if len < pad_to {
ui.add_space((pad_to - len) as f32 * space_width);
}
ret
}
#[must_use]
fn asm_row_ui(
ui: &mut egui::Ui,
ins_diff: &ObjInsDiff,
symbol: &ObjSymbol,
appearance: &Appearance,
ins_view_state: &FunctionViewState,
column: usize,
response_cb: impl Fn(Response) -> Response,
) -> Option<DiffViewAction> {
let mut ret = None;
ui.spacing_mut().item_spacing.x = 0.0;
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
if ins_diff.kind != ObjInsDiffKind::None {
ui.painter().rect_filled(ui.available_rect_before_wrap(), 0.0, ui.visuals().faint_bg_color);
}
let space_width = ui.fonts(|f| f.glyph_width(&appearance.code_font, ' '));
display_diff(ins_diff, symbol.address, |text| {
if let Some(action) = diff_text_ui(
ui,
text,
ins_diff,
appearance,
ins_view_state,
column,
space_width,
&response_cb,
) {
ret = Some(action);
}
Ok::<_, ()>(())
})
.unwrap();
ret
}
#[must_use]
fn asm_col_ui(
row: &mut TableRow<'_, '_>,
ctx: FunctionDiffContext<'_>,
appearance: &Appearance,
ins_view_state: &FunctionViewState,
column: usize,
) -> Option<DiffViewAction> {
let mut ret = None;
let symbol_ref = ctx.symbol_ref?;
let (section, symbol) = ctx.obj.section_symbol(symbol_ref);
let section = section?;
let ins_diff = &ctx.diff.symbol_diff(symbol_ref).instructions[row.index()];
let response_cb = |response: Response| {
if let Some(ins) = &ins_diff.ins {
response.context_menu(|ui| ins_context_menu(ui, section, ins, symbol));
response.on_hover_ui_at_pointer(|ui| {
ins_hover_ui(ui, ctx.obj, section, ins, symbol, appearance)
})
} else {
response
}
};
let (_, response) = row.col(|ui| {
if let Some(action) =
asm_row_ui(ui, ins_diff, symbol, appearance, ins_view_state, column, response_cb)
{
ret = Some(action);
}
});
response_cb(response);
ret
}
#[must_use]
fn asm_table_ui(
ui: &mut egui::Ui,
available_width: f32,
left_ctx: Option<FunctionDiffContext<'_>>,
right_ctx: Option<FunctionDiffContext<'_>>,
appearance: &Appearance,
ins_view_state: &FunctionViewState,
symbol_state: &SymbolViewState,
) -> Option<DiffViewAction> {
let mut ret = None;
let left_len = left_ctx.and_then(|ctx| {
ctx.symbol_ref.map(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).instructions.len())
});
let right_len = right_ctx.and_then(|ctx| {
ctx.symbol_ref.map(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).instructions.len())
});
let instructions_len = match (left_len, right_len) {
(Some(left_len), Some(right_len)) => {
if left_len != right_len {
ui.label("Instruction count mismatch");
return None;
}
left_len
}
(Some(left_len), None) => left_len,
(None, Some(right_len)) => right_len,
(None, None) => {
ui.label("No symbol selected");
return None;
}
};
if left_len.is_some() && right_len.is_some() {
// Joint view
render_table(
ui,
available_width,
2,
appearance.code_font.size,
instructions_len,
|row, column| {
if column == 0 {
if let Some(ctx) = left_ctx {
if let Some(action) =
asm_col_ui(row, ctx, appearance, ins_view_state, column)
{
ret = Some(action);
}
}
} else if column == 1 {
if let Some(ctx) = right_ctx {
if let Some(action) =
asm_col_ui(row, ctx, appearance, ins_view_state, column)
{
ret = Some(action);
}
}
if row.response().clicked() {
ret = Some(DiffViewAction::ClearDiffHighlight);
}
}
},
);
} else {
// Split view, one side is the symbol list
render_strips(ui, available_width, 2, |ui, column| {
if column == 0 {
if let Some(ctx) = left_ctx {
if ctx.has_symbol() {
render_table(
ui,
available_width / 2.0,
1,
appearance.code_font.size,
instructions_len,
|row, column| {
if let Some(action) =
asm_col_ui(row, ctx, appearance, ins_view_state, column)
{
ret = Some(action);
}
if row.response().clicked() {
ret = Some(DiffViewAction::ClearDiffHighlight);
}
},
);
} else if let Some((right_ctx, right_symbol_ref)) =
right_ctx.and_then(|ctx| ctx.symbol_ref.map(|symbol_ref| (ctx, symbol_ref)))
{
if let Some(action) = symbol_list_ui(
ui,
SymbolDiffContext { obj: ctx.obj, diff: ctx.diff },
None,
symbol_state,
SymbolFilter::Mapping(right_symbol_ref),
appearance,
column,
) {
match action {
DiffViewAction::Navigate(DiffViewNavigation {
left_symbol: Some(left_symbol_ref),
..
}) => {
let (right_section, right_symbol) =
right_ctx.obj.section_symbol(right_symbol_ref);
ret = Some(DiffViewAction::SetMapping(
match right_section.map(|s| s.kind) {
Some(ObjSectionKind::Code) => View::FunctionDiff,
_ => View::SymbolDiff,
},
left_symbol_ref,
SymbolRefByName::new(right_symbol, right_section),
));
}
DiffViewAction::SetSymbolHighlight(_, _) => {
// Ignore
}
_ => {
ret = Some(action);
}
}
}
}
} else {
ui.label("No left object");
}
} else if column == 1 {
if let Some(ctx) = right_ctx {
if ctx.has_symbol() {
render_table(
ui,
available_width / 2.0,
1,
appearance.code_font.size,
instructions_len,
|row, column| {
if let Some(action) =
asm_col_ui(row, ctx, appearance, ins_view_state, column)
{
ret = Some(action);
}
if row.response().clicked() {
ret = Some(DiffViewAction::ClearDiffHighlight);
}
},
);
} else if let Some((left_ctx, left_symbol_ref)) =
left_ctx.and_then(|ctx| ctx.symbol_ref.map(|symbol_ref| (ctx, symbol_ref)))
{
if let Some(action) = symbol_list_ui(
ui,
SymbolDiffContext { obj: ctx.obj, diff: ctx.diff },
None,
symbol_state,
SymbolFilter::Mapping(left_symbol_ref),
appearance,
column,
) {
match action {
DiffViewAction::Navigate(DiffViewNavigation {
right_symbol: Some(right_symbol_ref),
..
}) => {
let (left_section, left_symbol) =
left_ctx.obj.section_symbol(left_symbol_ref);
ret = Some(DiffViewAction::SetMapping(
match left_section.map(|s| s.kind) {
Some(ObjSectionKind::Code) => View::FunctionDiff,
_ => View::SymbolDiff,
},
SymbolRefByName::new(left_symbol, left_section),
right_symbol_ref,
));
}
DiffViewAction::SetSymbolHighlight(_, _) => {
// Ignore
}
_ => {
ret = Some(action);
}
}
}
}
} else {
ui.label("No right object");
}
}
});
}
ret
}
#[derive(Clone, Copy)]
pub struct FunctionDiffContext<'a> {
pub obj: &'a ObjInfo,
pub diff: &'a ObjDiff,
pub symbol_ref: Option<SymbolRef>,
}
impl<'a> FunctionDiffContext<'a> {
pub fn new(
obj: Option<&'a (ObjInfo, ObjDiff)>,
selected_symbol: Option<&SymbolRefByName>,
) -> Option<Self> {
obj.map(|(obj, diff)| Self {
obj,
diff,
symbol_ref: selected_symbol.and_then(|s| find_symbol(obj, s)),
})
}
#[inline]
pub fn has_symbol(&self) -> bool { self.symbol_ref.is_some() }
}
#[must_use]
pub fn function_diff_ui(
ui: &mut egui::Ui,
state: &DiffViewState,
appearance: &Appearance,
) -> Option<DiffViewAction> {
let mut ret = None;
let Some(result) = &state.build else {
return ret;
};
let mut left_ctx = FunctionDiffContext::new(
result.first_obj.as_ref(),
state.symbol_state.left_symbol.as_ref(),
);
let mut right_ctx = FunctionDiffContext::new(
result.second_obj.as_ref(),
state.symbol_state.right_symbol.as_ref(),
);
// If one side is missing a symbol, but the diff process found a match, use that symbol
let left_diff_symbol = left_ctx.and_then(|ctx| {
ctx.symbol_ref.and_then(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).target_symbol)
});
let right_diff_symbol = right_ctx.and_then(|ctx| {
ctx.symbol_ref.and_then(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).target_symbol)
});
if left_diff_symbol.is_some() && right_ctx.is_some_and(|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.is_some_and(|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.is_some_and(|ctx| !ctx.has_symbol())
&& left_ctx.is_some_and(|ctx| !ctx.has_symbol())
{
return Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
}
// Header
let available_width = ui.available_width();
render_header(ui, available_width, 2, |ui, column| {
if column == 0 {
// Left column
ui.horizontal(|ui| {
if ui.button("⏴ Back").clicked() {
ret = Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
}
ui.separator();
if ui
.add_enabled(
!state.scratch_running
&& state.scratch_available
&& left_ctx.is_some_and(|ctx| ctx.has_symbol()),
egui::Button::new("📲 decomp.me"),
)
.on_hover_text_at_pointer("Create a new scratch on decomp.me (beta)")
.on_disabled_hover_text("Scratch configuration missing")
.clicked()
{
if let Some((_section, symbol)) = left_ctx.and_then(|ctx| {
ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref))
}) {
ret = Some(DiffViewAction::CreateScratch(symbol.name.clone()));
}
}
});
if let Some((_section, symbol)) = left_ctx
.and_then(|ctx| ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref)))
{
let name = symbol.demangled_name.as_deref().unwrap_or(&symbol.name);
ui.label(
RichText::new(name)
.font(appearance.code_font.clone())
.color(appearance.highlight_color),
);
if right_ctx.is_some_and(|m| m.has_symbol())
&& ui
.button("Change target")
.on_hover_text_at_pointer("Choose a different symbol to use as the target")
.clicked()
{
if let Some(symbol_ref) = state.symbol_state.right_symbol.as_ref() {
ret = Some(DiffViewAction::SelectingLeft(symbol_ref.clone()));
}
}
} else {
ui.label(
RichText::new("Missing")
.font(appearance.code_font.clone())
.color(appearance.replace_color),
);
ui.label(
RichText::new("Choose target symbol")
.font(appearance.code_font.clone())
.color(appearance.highlight_color),
);
}
} else if column == 1 {
// Right column
ui.horizontal(|ui| {
if ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() {
ret = Some(DiffViewAction::Build);
}
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
if state.build_running {
ui.colored_label(appearance.replace_color, "Building…");
} else {
ui.label("Last built:");
let format = format_description::parse("[hour]:[minute]:[second]").unwrap();
ui.label(
result.time.to_offset(appearance.utc_offset).format(&format).unwrap(),
);
}
});
ui.separator();
if ui
.add_enabled(state.source_path_available, egui::Button::new("🖹 Source file"))
.on_hover_text_at_pointer("Open the source file in the default editor")
.on_disabled_hover_text("Source file metadata missing")
.clicked()
{
ret = Some(DiffViewAction::OpenSourcePath);
}
});
if let Some(((_section, symbol), symbol_diff)) = right_ctx.and_then(|ctx| {
ctx.symbol_ref.map(|symbol_ref| {
(ctx.obj.section_symbol(symbol_ref), ctx.diff.symbol_diff(symbol_ref))
})
}) {
let name = symbol.demangled_name.as_deref().unwrap_or(&symbol.name);
ui.label(
RichText::new(name)
.font(appearance.code_font.clone())
.color(appearance.highlight_color),
);
ui.horizontal(|ui| {
if let Some(match_percent) = symbol_diff.match_percent {
ui.label(
RichText::new(format!("{:.0}%", match_percent.floor()))
.font(appearance.code_font.clone())
.color(match_color_for_symbol(match_percent, appearance)),
);
}
if left_ctx.is_some_and(|m| m.has_symbol()) {
ui.separator();
if ui
.button("Change base")
.on_hover_text_at_pointer(
"Choose a different symbol to use as the base",
)
.clicked()
{
if let Some(symbol_ref) = state.symbol_state.left_symbol.as_ref() {
ret = Some(DiffViewAction::SelectingRight(symbol_ref.clone()));
}
}
}
});
} else {
ui.label(
RichText::new("Missing")
.font(appearance.code_font.clone())
.color(appearance.replace_color),
);
ui.label(
RichText::new("Choose base symbol")
.font(appearance.code_font.clone())
.color(appearance.highlight_color),
);
}
}
});
// Table
let id = Id::new(state.symbol_state.left_symbol.as_ref().map(|s| s.symbol_name.as_str()))
.with(state.symbol_state.right_symbol.as_ref().map(|s| s.symbol_name.as_str()));
if let Some(action) = ui
.push_id(id, |ui| {
asm_table_ui(
ui,
available_width,
left_ctx,
right_ctx,
appearance,
&state.function_state,
&state.symbol_state,
)
})
.inner
{
ret = Some(action);
}
ret
}

View File

@@ -0,0 +1,159 @@
use std::{
fs::File,
io::{BufReader, BufWriter},
path::{Path, PathBuf},
};
use anyhow::Result;
use egui::{text::LayoutJob, Context, FontId, RichText, TextFormat, TextStyle, Window};
use serde::{Deserialize, Serialize};
use strum::{EnumIter, EnumMessage, IntoEnumIterator};
use crate::views::{appearance::Appearance, frame_history::FrameHistory};
#[derive(Default)]
pub struct GraphicsViewState {
pub active_backend: String,
pub active_device: String,
pub graphics_config: GraphicsConfig,
pub graphics_config_path: Option<PathBuf>,
pub should_relaunch: bool,
}
#[derive(
Copy, Clone, Debug, Default, PartialEq, Eq, EnumIter, EnumMessage, Serialize, Deserialize,
)]
pub enum GraphicsBackend {
#[default]
#[strum(message = "Auto")]
Auto,
#[strum(message = "Vulkan")]
Vulkan,
#[strum(message = "Metal")]
Metal,
#[strum(message = "DirectX 12")]
Dx12,
#[strum(message = "OpenGL")]
OpenGL,
}
#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)]
pub struct GraphicsConfig {
#[serde(default)]
pub desired_backend: GraphicsBackend,
}
pub fn load_graphics_config(path: &Path) -> Result<Option<GraphicsConfig>> {
if !path.exists() {
return Ok(None);
}
let file = BufReader::new(File::open(path)?);
let config: GraphicsConfig = ron::de::from_reader(file)?;
Ok(Some(config))
}
pub fn save_graphics_config(path: &Path, config: &GraphicsConfig) -> Result<()> {
let file = BufWriter::new(File::create(path)?);
ron::ser::to_writer(file, config)?;
Ok(())
}
impl GraphicsBackend {
pub fn is_supported(&self) -> bool {
match self {
GraphicsBackend::Auto => true,
GraphicsBackend::Vulkan => {
cfg!(all(feature = "wgpu", any(target_os = "windows", target_os = "linux")))
}
GraphicsBackend::Metal => cfg!(all(feature = "wgpu", target_os = "macos")),
GraphicsBackend::Dx12 => cfg!(all(feature = "wgpu", target_os = "windows")),
GraphicsBackend::OpenGL => true,
}
}
}
pub fn graphics_window(
ctx: &Context,
show: &mut bool,
frame_history: &mut FrameHistory,
state: &mut GraphicsViewState,
appearance: &Appearance,
) {
Window::new("Graphics").open(show).show(ctx, |ui| {
ui.label("Graphics backend:");
ui.label(
RichText::new(&state.active_backend)
.color(appearance.emphasized_text_color)
.text_style(TextStyle::Monospace),
);
ui.label("Graphics device:");
ui.label(
RichText::new(&state.active_device)
.color(appearance.emphasized_text_color)
.text_style(TextStyle::Monospace),
);
ui.label(format!("FPS: {:.1}", frame_history.fps()));
ui.separator();
let mut job = LayoutJob::default();
job.append(
"WARNING: ",
0.0,
TextFormat::simple(appearance.ui_font.clone(), appearance.delete_color),
);
job.append(
"Changing the graphics backend may cause the application\nto no longer start or display correctly. Use with caution!",
0.0,
TextFormat::simple(appearance.ui_font.clone(), appearance.emphasized_text_color),
);
if let Some(config_path) = &state.graphics_config_path {
job.append(
"\n\nDelete the following file to reset:\n",
0.0,
TextFormat::simple(appearance.ui_font.clone(), appearance.emphasized_text_color),
);
job.append(
config_path.to_string_lossy().as_ref(),
0.0,
TextFormat::simple(
FontId {
family: appearance.code_font.family.clone(),
size: appearance.ui_font.size,
},
appearance.emphasized_text_color,
),
);
}
job.append(
"\n\nChanging the graphics backend will restart the application.",
0.0,
TextFormat::simple(appearance.ui_font.clone(), appearance.replace_color),
);
ui.label(job);
ui.add_enabled_ui(state.graphics_config_path.is_some(), |ui| {
ui.horizontal(|ui| {
ui.label("Desired backend:");
for backend in GraphicsBackend::iter().filter(GraphicsBackend::is_supported) {
let selected = state.graphics_config.desired_backend == backend;
if ui.selectable_label(selected, backend.get_message().unwrap()).clicked() {
let prev_backend = state.graphics_config.desired_backend;
state.graphics_config.desired_backend = backend;
match save_graphics_config(
state.graphics_config_path.as_ref().unwrap(),
&state.graphics_config,
) {
Ok(()) => {
state.should_relaunch = true;
}
Err(e) => {
log::error!("Failed to save graphics config: {:?}", e);
state.graphics_config.desired_backend = prev_backend;
}
}
}
}
});
});
});
}

View File

@@ -0,0 +1,162 @@
use std::cmp::Ordering;
use egui::{ProgressBar, RichText, Widget};
use crate::{
jobs::{JobQueue, JobStatus},
views::appearance::Appearance,
};
pub fn jobs_ui(ui: &mut egui::Ui, jobs: &mut JobQueue, appearance: &Appearance) {
if ui.button("Clear").clicked() {
jobs.clear_errored();
}
let mut remove_job: Option<usize> = None;
let mut any_jobs = false;
for job in jobs.iter_mut() {
let Ok(status) = job.context.status.read() else {
continue;
};
any_jobs = true;
ui.separator();
ui.horizontal(|ui| {
ui.label(&status.title);
if ui.small_button("").clicked() {
if job.handle.is_some() {
if let Err(e) = job.cancel.send(()) {
log::error!("Failed to cancel job: {e:?}");
}
} else {
remove_job = Some(job.id);
}
}
});
let mut bar = ProgressBar::new(status.progress_percent);
if let Some(items) = &status.progress_items {
bar = bar.text(format!("{} / {}", items[0], items[1]));
}
bar.ui(ui);
const STATUS_LENGTH: usize = 80;
if let Some(err) = &status.error {
let err_string = format!("{:#}", err);
ui.colored_label(
appearance.delete_color,
if err_string.len() > STATUS_LENGTH - 10 {
format!("Error: {}", &err_string[0..STATUS_LENGTH - 10])
} else {
format!("Error: {:width$}", err_string, width = STATUS_LENGTH - 7)
},
)
.on_hover_text_at_pointer(RichText::new(&err_string).color(appearance.delete_color))
.context_menu(|ui| {
if ui.button("Copy full message").clicked() {
ui.output_mut(|o| o.copied_text = err_string);
}
});
} else {
ui.label(if status.status.len() > STATUS_LENGTH - 3 {
format!("{}", &status.status[0..STATUS_LENGTH - 3])
} else {
format!("{:width$}", &status.status, width = STATUS_LENGTH)
})
.on_hover_text_at_pointer(&status.status)
.context_menu(|ui| {
if ui.button("Copy full message").clicked() {
ui.output_mut(|o| o.copied_text = status.status.clone());
}
});
}
}
if !any_jobs {
ui.label("No jobs");
}
if let Some(idx) = remove_job {
jobs.remove(idx);
}
}
struct JobStatusDisplay {
title: String,
progress_items: Option<[u32; 2]>,
error: bool,
}
impl From<&JobStatus> for JobStatusDisplay {
fn from(status: &JobStatus) -> Self {
Self {
title: status.title.clone(),
progress_items: status.progress_items,
error: status.error.is_some(),
}
}
}
pub fn jobs_menu_ui(ui: &mut egui::Ui, jobs: &mut JobQueue, appearance: &Appearance) -> bool {
ui.label("Jobs:");
let mut statuses = Vec::new();
for job in jobs.iter_mut() {
let Ok(status) = job.context.status.read() else {
continue;
};
statuses.push(JobStatusDisplay::from(&*status));
}
let running_jobs = statuses.iter().filter(|s| !s.error).count();
let error_jobs = statuses.iter().filter(|s| s.error).count();
let mut clicked = false;
let spinner =
egui::Spinner::new().size(appearance.ui_font.size * 0.9).color(appearance.text_color);
match running_jobs.cmp(&1) {
Ordering::Equal => {
spinner.ui(ui);
let running_job = statuses.iter().find(|s| !s.error).unwrap();
let text = if let Some(items) = running_job.progress_items {
format!("{} ({}/{})", running_job.title, items[0], items[1])
} else {
running_job.title.clone()
};
clicked |= ui.link(RichText::new(text)).clicked();
}
Ordering::Greater => {
spinner.ui(ui);
clicked |= ui.link(format!("{} running", running_jobs)).clicked();
}
_ => (),
}
match error_jobs.cmp(&1) {
Ordering::Equal => {
let error_job = statuses.iter().find(|s| s.error).unwrap();
clicked |= ui
.link(
RichText::new(format!("{} error", error_job.title))
.color(appearance.delete_color),
)
.clicked();
}
Ordering::Greater => {
clicked |= ui
.link(
RichText::new(format!("{} errors", error_jobs)).color(appearance.delete_color),
)
.clicked();
}
_ => (),
}
if running_jobs == 0 && error_jobs == 0 {
clicked |= ui.link("None").clicked();
}
clicked
}
pub fn jobs_window(
ctx: &egui::Context,
show: &mut bool,
jobs: &mut JobQueue,
appearance: &Appearance,
) {
egui::Window::new("Jobs").open(show).show(ctx, |ui| {
jobs_ui(ui, jobs, appearance);
});
}

View File

@@ -0,0 +1,21 @@
use egui::{text::LayoutJob, Color32, FontId, TextFormat};
pub(crate) mod appearance;
pub(crate) mod column_layout;
pub(crate) mod config;
pub(crate) mod data_diff;
pub(crate) mod debug;
pub(crate) mod demangle;
pub(crate) mod extab_diff;
pub(crate) mod file;
pub(crate) mod frame_history;
pub(crate) mod function_diff;
pub(crate) mod graphics;
pub(crate) mod jobs;
pub(crate) mod rlwinm;
pub(crate) mod symbol_diff;
#[inline]
fn write_text(str: &str, color: Color32, job: &mut LayoutJob, font_id: FontId) {
job.append(str, 0.0, TextFormat::simple(font_id, color));
}

View File

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

View File

@@ -0,0 +1,938 @@
use std::{collections::BTreeMap, mem::take};
use egui::{
text::LayoutJob, CollapsingHeader, Color32, Id, OpenUrl, ScrollArea, SelectableLabel, TextEdit,
Ui, Widget,
};
use objdiff_core::{
arch::ObjArch,
diff::{display::HighlightKind, ObjDiff, ObjSymbolDiff},
obj::{
ObjInfo, ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlags, SymbolRef, SECTION_COMMON,
},
};
use regex::{Regex, RegexBuilder};
use crate::{
app::AppStateRef,
jobs::{
create_scratch::{start_create_scratch, CreateScratchConfig, CreateScratchResult},
objdiff::{BuildStatus, ObjDiffResult},
Job, JobQueue, JobResult,
},
views::{
appearance::Appearance,
column_layout::{render_header, render_strips},
function_diff::FunctionViewState,
write_text,
},
};
#[derive(Debug, Clone)]
pub struct SymbolRefByName {
pub symbol_name: String,
pub section_name: Option<String>,
}
impl SymbolRefByName {
pub fn new(symbol: &ObjSymbol, section: Option<&ObjSection>) -> Self {
Self { symbol_name: symbol.name.clone(), section_name: section.map(|s| s.name.clone()) }
}
}
#[expect(clippy::enum_variant_names)]
#[derive(Debug, Default, Eq, PartialEq, Copy, Clone, Hash)]
pub enum View {
#[default]
SymbolDiff,
FunctionDiff,
DataDiff,
ExtabDiff,
}
#[derive(Debug, Clone)]
pub enum DiffViewAction {
/// Queue a rebuild of the current object(s)
Build,
/// Navigate to a new diff view
Navigate(DiffViewNavigation),
/// Set the highlighted symbols in the symbols view
SetSymbolHighlight(Option<SymbolRef>, Option<SymbolRef>),
/// Set the symbols view search filter
SetSearch(String),
/// Submit the current function to decomp.me
CreateScratch(String),
/// Open the source path of the current object
OpenSourcePath,
/// Set the highlight for a diff column
SetDiffHighlight(usize, HighlightKind),
/// Clear the highlight for all diff columns
ClearDiffHighlight,
/// Start selecting a left symbol for mapping.
/// The symbol reference is the right symbol to map to.
SelectingLeft(SymbolRefByName),
/// Start selecting a right symbol for mapping.
/// The symbol reference is the left symbol to map to.
SelectingRight(SymbolRefByName),
/// Set a symbol mapping.
SetMapping(View, SymbolRefByName, SymbolRefByName),
/// Set the show_mapped_symbols flag
SetShowMappedSymbols(bool),
}
#[derive(Debug, Clone, Default)]
pub struct DiffViewNavigation {
pub view: Option<View>,
pub left_symbol: Option<SymbolRefByName>,
pub right_symbol: Option<SymbolRefByName>,
}
impl DiffViewNavigation {
pub fn symbol_diff() -> Self {
Self { view: Some(View::SymbolDiff), left_symbol: None, right_symbol: None }
}
pub fn with_symbols(
view: View,
other_ctx: Option<SymbolDiffContext<'_>>,
symbol: &ObjSymbol,
section: &ObjSection,
symbol_diff: &ObjSymbolDiff,
column: usize,
) -> Self {
let symbol1 = Some(SymbolRefByName::new(symbol, Some(section)));
let symbol2 = symbol_diff.target_symbol.and_then(|symbol_ref| {
other_ctx.map(|ctx| {
let (section, symbol) = ctx.obj.section_symbol(symbol_ref);
SymbolRefByName::new(symbol, section)
})
});
match column {
0 => Self { view: Some(view), left_symbol: symbol1, right_symbol: symbol2 },
1 => Self { view: Some(view), left_symbol: symbol2, right_symbol: symbol1 },
_ => unreachable!("Invalid column index"),
}
}
}
#[derive(Default)]
pub struct DiffViewState {
pub build: Option<Box<ObjDiffResult>>,
pub scratch: Option<Box<CreateScratchResult>>,
pub current_view: View,
pub symbol_state: SymbolViewState,
pub function_state: FunctionViewState,
pub search: String,
pub search_regex: Option<Regex>,
pub build_running: bool,
pub scratch_available: bool,
pub scratch_running: bool,
pub source_path_available: bool,
pub post_build_nav: Option<DiffViewNavigation>,
pub object_name: String,
}
#[derive(Default)]
pub struct SymbolViewState {
pub highlighted_symbol: (Option<SymbolRef>, Option<SymbolRef>),
pub left_symbol: Option<SymbolRefByName>,
pub right_symbol: Option<SymbolRefByName>,
pub reverse_fn_order: bool,
pub disable_reverse_fn_order: bool,
pub show_hidden_symbols: bool,
pub show_mapped_symbols: bool,
}
impl DiffViewState {
pub fn pre_update(&mut self, jobs: &mut JobQueue, state: &AppStateRef) {
jobs.results.retain_mut(|result| match result {
JobResult::ObjDiff(result) => {
self.build = take(result);
// TODO: where should this go?
if let Some(result) = self.post_build_nav.take() {
if let Some(view) = result.view {
self.current_view = view;
}
self.symbol_state.left_symbol = result.left_symbol;
self.symbol_state.right_symbol = result.right_symbol;
}
false
}
JobResult::CreateScratch(result) => {
self.scratch = take(result);
false
}
_ => true,
});
self.build_running = jobs.is_running(Job::ObjDiff);
self.scratch_running = jobs.is_running(Job::CreateScratch);
self.symbol_state.disable_reverse_fn_order = false;
if let Ok(state) = state.read() {
if let Some(obj_config) = &state.config.selected_obj {
if let Some(value) = obj_config.reverse_fn_order {
self.symbol_state.reverse_fn_order = value;
self.symbol_state.disable_reverse_fn_order = true;
}
self.source_path_available = obj_config.source_path.is_some();
} else {
self.source_path_available = false;
}
self.scratch_available = CreateScratchConfig::is_available(&state.config);
self.object_name =
state.config.selected_obj.as_ref().map(|o| o.name.clone()).unwrap_or_default();
}
}
pub fn post_update(
&mut self,
action: Option<DiffViewAction>,
ctx: &egui::Context,
jobs: &mut JobQueue,
state: &AppStateRef,
) {
if let Some(result) = take(&mut self.scratch) {
ctx.output_mut(|o| o.open_url = Some(OpenUrl::new_tab(result.scratch_url)));
}
let Some(action) = action else {
return;
};
match action {
DiffViewAction::Build => {
if let Ok(mut state) = state.write() {
state.queue_build = true;
}
}
DiffViewAction::Navigate(nav) => {
if self.post_build_nav.is_some() {
// Ignore action if we're already navigating
return;
}
self.symbol_state.highlighted_symbol = (None, None);
let Ok(mut state) = state.write() else {
return;
};
if (nav.left_symbol.is_some() && nav.right_symbol.is_some())
|| (nav.left_symbol.is_none() && nav.right_symbol.is_none())
|| nav.view != Some(View::FunctionDiff)
{
// Regular navigation
if state.is_selecting_symbol() {
// Cancel selection and reload
state.clear_selection();
self.post_build_nav = Some(nav);
} else {
// Navigate immediately
if let Some(view) = nav.view {
self.current_view = view;
}
self.symbol_state.left_symbol = nav.left_symbol;
self.symbol_state.right_symbol = nav.right_symbol;
}
} else {
// Enter selection mode
match (&nav.left_symbol, &nav.right_symbol) {
(Some(left_ref), None) => {
state.set_selecting_right(&left_ref.symbol_name);
}
(None, Some(right_ref)) => {
state.set_selecting_left(&right_ref.symbol_name);
}
(Some(_), Some(_)) => unreachable!(),
(None, None) => unreachable!(),
}
self.post_build_nav = Some(nav);
}
}
DiffViewAction::SetSymbolHighlight(left, right) => {
self.symbol_state.highlighted_symbol = (left, right);
}
DiffViewAction::SetSearch(search) => {
self.search_regex = if search.is_empty() {
None
} else if let Ok(regex) = RegexBuilder::new(&search).case_insensitive(true).build()
{
Some(regex)
} else {
None
};
self.search = search;
}
DiffViewAction::CreateScratch(function_name) => {
let Ok(state) = state.read() else {
return;
};
match CreateScratchConfig::from_config(&state.config, function_name) {
Ok(config) => {
jobs.push_once(Job::CreateScratch, || start_create_scratch(ctx, config));
}
Err(err) => {
log::error!("Failed to create scratch config: {err}");
}
}
}
DiffViewAction::OpenSourcePath => {
let Ok(state) = state.read() else {
return;
};
if let (Some(project_dir), Some(source_path)) = (
&state.config.project_dir,
state.config.selected_obj.as_ref().and_then(|obj| obj.source_path.as_ref()),
) {
let source_path = project_dir.join(source_path);
log::info!("Opening file {}", source_path.display());
open::that_detached(source_path).unwrap_or_else(|err| {
log::error!("Failed to open source file: {err}");
});
}
}
DiffViewAction::SetDiffHighlight(column, kind) => {
self.function_state.set_highlight(column, kind);
}
DiffViewAction::ClearDiffHighlight => {
self.function_state.clear_highlight();
}
DiffViewAction::SelectingLeft(right_ref) => {
if self.post_build_nav.is_some() {
// Ignore action if we're already navigating
return;
}
let Ok(mut state) = state.write() else {
return;
};
state.set_selecting_left(&right_ref.symbol_name);
self.post_build_nav = Some(DiffViewNavigation {
view: Some(View::FunctionDiff),
left_symbol: None,
right_symbol: Some(right_ref),
});
}
DiffViewAction::SelectingRight(left_ref) => {
if self.post_build_nav.is_some() {
// Ignore action if we're already navigating
return;
}
let Ok(mut state) = state.write() else {
return;
};
state.set_selecting_right(&left_ref.symbol_name);
self.post_build_nav = Some(DiffViewNavigation {
view: Some(View::FunctionDiff),
left_symbol: Some(left_ref),
right_symbol: None,
});
}
DiffViewAction::SetMapping(view, left_ref, right_ref) => {
if self.post_build_nav.is_some() {
// Ignore action if we're already navigating
return;
}
let Ok(mut state) = state.write() else {
return;
};
state.set_symbol_mapping(
left_ref.symbol_name.clone(),
right_ref.symbol_name.clone(),
);
if view == View::SymbolDiff {
self.post_build_nav = Some(DiffViewNavigation::symbol_diff());
} else {
self.post_build_nav = Some(DiffViewNavigation {
view: Some(view),
left_symbol: Some(left_ref),
right_symbol: Some(right_ref),
});
}
}
DiffViewAction::SetShowMappedSymbols(value) => {
self.symbol_state.show_mapped_symbols = value;
}
}
}
}
pub fn match_color_for_symbol(match_percent: f32, appearance: &Appearance) -> Color32 {
if match_percent == 100.0 {
appearance.insert_color
} else if match_percent >= 50.0 {
appearance.replace_color
} else {
appearance.delete_color
}
}
fn symbol_context_menu_ui(
ui: &mut Ui,
ctx: SymbolDiffContext<'_>,
other_ctx: Option<SymbolDiffContext<'_>>,
symbol: &ObjSymbol,
symbol_diff: &ObjSymbolDiff,
section: Option<&ObjSection>,
column: usize,
) -> Option<DiffViewNavigation> {
let mut ret = None;
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
if let Some(name) = &symbol.demangled_name {
if ui.button(format!("Copy \"{name}\"")).clicked() {
ui.output_mut(|output| output.copied_text.clone_from(name));
ui.close_menu();
}
}
if ui.button(format!("Copy \"{}\"", symbol.name)).clicked() {
ui.output_mut(|output| output.copied_text.clone_from(&symbol.name));
ui.close_menu();
}
if let Some(address) = symbol.virtual_address {
if ui.button(format!("Copy \"{:#x}\" (virtual address)", address)).clicked() {
ui.output_mut(|output| output.copied_text = format!("{:#x}", address));
ui.close_menu();
}
}
if let Some(section) = section {
let has_extab =
ctx.obj.arch.ppc().and_then(|ppc| ppc.extab_for_symbol(symbol)).is_some();
if has_extab && ui.button("Decode exception table").clicked() {
ret = Some(DiffViewNavigation::with_symbols(
View::ExtabDiff,
other_ctx,
symbol,
section,
symbol_diff,
column,
));
ui.close_menu();
}
if ui.button("Map symbol").clicked() {
let symbol_ref = SymbolRefByName::new(symbol, Some(section));
if column == 0 {
ret = Some(DiffViewNavigation {
view: Some(View::FunctionDiff),
left_symbol: Some(symbol_ref),
right_symbol: None,
});
} else {
ret = Some(DiffViewNavigation {
view: Some(View::FunctionDiff),
left_symbol: None,
right_symbol: Some(symbol_ref),
});
}
ui.close_menu();
}
}
});
ret
}
fn symbol_hover_ui(ui: &mut Ui, arch: &dyn ObjArch, symbol: &ObjSymbol, appearance: &Appearance) {
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
ui.colored_label(appearance.highlight_color, format!("Name: {}", symbol.name));
ui.colored_label(appearance.highlight_color, format!("Address: {:x}", symbol.address));
if symbol.size_known {
ui.colored_label(appearance.highlight_color, format!("Size: {:x}", symbol.size));
} else {
ui.colored_label(
appearance.highlight_color,
format!("Size: {:x} (assumed)", symbol.size),
);
}
if let Some(address) = symbol.virtual_address {
ui.colored_label(appearance.replace_color, format!("Virtual address: {:#x}", address));
}
if let Some(extab) = arch.ppc().and_then(|ppc| ppc.extab_for_symbol(symbol)) {
ui.colored_label(
appearance.highlight_color,
format!("extab symbol: {}", &extab.etb_symbol.name),
);
ui.colored_label(
appearance.highlight_color,
format!("extabindex symbol: {}", &extab.eti_symbol.name),
);
}
});
}
#[must_use]
#[expect(clippy::too_many_arguments)]
fn symbol_ui(
ui: &mut Ui,
ctx: SymbolDiffContext<'_>,
other_ctx: Option<SymbolDiffContext<'_>>,
symbol: &ObjSymbol,
symbol_diff: &ObjSymbolDiff,
section: Option<&ObjSection>,
state: &SymbolViewState,
appearance: &Appearance,
column: usize,
) -> Option<DiffViewAction> {
let mut ret = None;
if symbol.flags.0.contains(ObjSymbolFlags::Hidden) && !state.show_hidden_symbols {
return ret;
}
let mut job = LayoutJob::default();
let name: &str =
if let Some(demangled) = &symbol.demangled_name { demangled } else { &symbol.name };
let mut selected = false;
if let Some(sym_ref) =
if column == 0 { state.highlighted_symbol.0 } else { state.highlighted_symbol.1 }
{
selected = symbol_diff.symbol_ref == sym_ref;
}
if !symbol.flags.0.is_empty() {
write_text("[", appearance.text_color, &mut job, appearance.code_font.clone());
if symbol.flags.0.contains(ObjSymbolFlags::Common) {
write_text("c", appearance.replace_color, &mut job, appearance.code_font.clone());
} else if symbol.flags.0.contains(ObjSymbolFlags::Global) {
write_text("g", appearance.insert_color, &mut job, appearance.code_font.clone());
} else if symbol.flags.0.contains(ObjSymbolFlags::Local) {
write_text("l", appearance.text_color, &mut job, appearance.code_font.clone());
}
if symbol.flags.0.contains(ObjSymbolFlags::Weak) {
write_text("w", appearance.text_color, &mut job, appearance.code_font.clone());
}
if symbol.flags.0.contains(ObjSymbolFlags::HasExtra) {
write_text("e", appearance.text_color, &mut job, appearance.code_font.clone());
}
if symbol.flags.0.contains(ObjSymbolFlags::Hidden) {
write_text(
"h",
appearance.deemphasized_text_color,
&mut job,
appearance.code_font.clone(),
);
}
write_text("] ", appearance.text_color, &mut job, appearance.code_font.clone());
}
if let Some(match_percent) = symbol_diff.match_percent {
write_text("(", appearance.text_color, &mut job, appearance.code_font.clone());
write_text(
&format!("{:.0}%", match_percent.floor()),
match_color_for_symbol(match_percent, appearance),
&mut job,
appearance.code_font.clone(),
);
write_text(") ", appearance.text_color, &mut job, appearance.code_font.clone());
}
write_text(name, appearance.highlight_color, &mut job, appearance.code_font.clone());
let response = SelectableLabel::new(selected, job).ui(ui).on_hover_ui_at_pointer(|ui| {
symbol_hover_ui(ui, ctx.obj.arch.as_ref(), symbol, appearance)
});
response.context_menu(|ui| {
if let Some(result) =
symbol_context_menu_ui(ui, ctx, other_ctx, symbol, symbol_diff, section, column)
{
ret = Some(DiffViewAction::Navigate(result));
}
});
if response.clicked() {
if let Some(section) = section {
match section.kind {
ObjSectionKind::Code => {
ret = Some(DiffViewAction::Navigate(DiffViewNavigation::with_symbols(
View::FunctionDiff,
other_ctx,
symbol,
section,
symbol_diff,
column,
)));
}
ObjSectionKind::Data => {
ret = Some(DiffViewAction::Navigate(DiffViewNavigation::with_symbols(
View::DataDiff,
other_ctx,
symbol,
section,
symbol_diff,
column,
)));
}
ObjSectionKind::Bss => {}
}
}
} else if response.hovered() {
ret = Some(if let Some(target_symbol) = symbol_diff.target_symbol {
if column == 0 {
DiffViewAction::SetSymbolHighlight(
Some(symbol_diff.symbol_ref),
Some(target_symbol),
)
} else {
DiffViewAction::SetSymbolHighlight(
Some(target_symbol),
Some(symbol_diff.symbol_ref),
)
}
} else {
DiffViewAction::SetSymbolHighlight(None, None)
});
}
ret
}
fn symbol_matches_filter(
symbol: &ObjSymbol,
diff: &ObjSymbolDiff,
filter: SymbolFilter<'_>,
) -> bool {
match filter {
SymbolFilter::None => true,
SymbolFilter::Search(regex) => {
regex.is_match(&symbol.name)
|| symbol.demangled_name.as_ref().map(|s| regex.is_match(s)).unwrap_or(false)
}
SymbolFilter::Mapping(symbol_ref) => diff.target_symbol == Some(symbol_ref),
}
}
#[derive(Copy, Clone)]
pub enum SymbolFilter<'a> {
None,
Search(&'a Regex),
Mapping(SymbolRef),
}
#[must_use]
pub fn symbol_list_ui(
ui: &mut Ui,
ctx: SymbolDiffContext<'_>,
other_ctx: Option<SymbolDiffContext<'_>>,
state: &SymbolViewState,
filter: SymbolFilter<'_>,
appearance: &Appearance,
column: usize,
) -> Option<DiffViewAction> {
let mut ret = None;
ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| {
let mut mapping = BTreeMap::new();
if let SymbolFilter::Mapping(target_ref) = filter {
let mut show_mapped_symbols = state.show_mapped_symbols;
if ui.checkbox(&mut show_mapped_symbols, "Show mapped symbols").changed() {
ret = Some(DiffViewAction::SetShowMappedSymbols(show_mapped_symbols));
}
for mapping_diff in &ctx.diff.mapping_symbols {
if mapping_diff.target_symbol == Some(target_ref) {
if !show_mapped_symbols {
let symbol_diff = ctx.diff.symbol_diff(mapping_diff.symbol_ref);
if symbol_diff.target_symbol.is_some() {
continue;
}
}
mapping.insert(mapping_diff.symbol_ref, mapping_diff);
}
}
} else {
for (symbol, diff) in ctx.obj.common.iter().zip(&ctx.diff.common) {
if !symbol_matches_filter(symbol, diff, filter) {
continue;
}
mapping.insert(diff.symbol_ref, diff);
}
for (section, section_diff) in ctx.obj.sections.iter().zip(&ctx.diff.sections) {
for (symbol, symbol_diff) in section.symbols.iter().zip(&section_diff.symbols) {
if !symbol_matches_filter(symbol, symbol_diff, filter) {
continue;
}
mapping.insert(symbol_diff.symbol_ref, symbol_diff);
}
}
}
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
// Skip sections with all symbols filtered out
if mapping.keys().any(|symbol_ref| symbol_ref.section_idx == SECTION_COMMON) {
CollapsingHeader::new(".comm").default_open(true).show(ui, |ui| {
for (symbol_ref, symbol_diff) in mapping
.iter()
.filter(|(symbol_ref, _)| symbol_ref.section_idx == SECTION_COMMON)
{
let symbol = ctx.obj.section_symbol(*symbol_ref).1;
if let Some(result) = symbol_ui(
ui,
ctx,
other_ctx,
symbol,
symbol_diff,
None,
state,
appearance,
column,
) {
ret = Some(result);
}
}
});
}
for ((section_index, section), section_diff) in
ctx.obj.sections.iter().enumerate().zip(&ctx.diff.sections)
{
// Skip sections with all symbols filtered out
if !mapping.keys().any(|symbol_ref| symbol_ref.section_idx == section_index) {
continue;
}
let mut header = LayoutJob::simple_singleline(
format!("{} ({:x})", section.name, section.size),
appearance.code_font.clone(),
Color32::PLACEHOLDER,
);
if let Some(match_percent) = section_diff.match_percent {
write_text(
" (",
Color32::PLACEHOLDER,
&mut header,
appearance.code_font.clone(),
);
write_text(
&format!("{:.0}%", match_percent.floor()),
match_color_for_symbol(match_percent, appearance),
&mut header,
appearance.code_font.clone(),
);
write_text(
")",
Color32::PLACEHOLDER,
&mut header,
appearance.code_font.clone(),
);
}
CollapsingHeader::new(header)
.id_salt(Id::new(section.name.clone()).with(section.orig_index))
.default_open(true)
.show(ui, |ui| {
if section.kind == ObjSectionKind::Code && state.reverse_fn_order {
for (symbol, symbol_diff) in mapping
.iter()
.filter(|(symbol_ref, _)| symbol_ref.section_idx == section_index)
.rev()
{
let symbol = ctx.obj.section_symbol(*symbol).1;
if let Some(result) = symbol_ui(
ui,
ctx,
other_ctx,
symbol,
symbol_diff,
Some(section),
state,
appearance,
column,
) {
ret = Some(result);
}
}
} else {
for (symbol, symbol_diff) in mapping
.iter()
.filter(|(symbol_ref, _)| symbol_ref.section_idx == section_index)
{
let symbol = ctx.obj.section_symbol(*symbol).1;
if let Some(result) = symbol_ui(
ui,
ctx,
other_ctx,
symbol,
symbol_diff,
Some(section),
state,
appearance,
column,
) {
ret = Some(result);
}
}
}
});
}
});
});
ret
}
fn build_log_ui(ui: &mut Ui, status: &BuildStatus, appearance: &Appearance) {
ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| {
ui.horizontal(|ui| {
if !status.cmdline.is_empty() && ui.button("Copy command").clicked() {
ui.output_mut(|output| output.copied_text.clone_from(&status.cmdline));
}
if ui.button("Copy log").clicked() {
ui.output_mut(|output| {
output.copied_text = format!("{}\n{}", status.stdout, status.stderr)
});
}
});
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
if !status.cmdline.is_empty() {
ui.label(&status.cmdline);
}
if !status.stdout.is_empty() {
ui.colored_label(appearance.replace_color, &status.stdout);
}
if !status.stderr.is_empty() {
ui.colored_label(appearance.delete_color, &status.stderr);
}
});
});
}
fn missing_obj_ui(ui: &mut Ui, appearance: &Appearance) {
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
ui.colored_label(appearance.replace_color, "No object configured");
});
}
#[derive(Copy, Clone)]
pub struct SymbolDiffContext<'a> {
pub obj: &'a ObjInfo,
pub diff: &'a ObjDiff,
}
#[must_use]
pub fn symbol_diff_ui(
ui: &mut Ui,
state: &mut DiffViewState,
appearance: &Appearance,
) -> Option<DiffViewAction> {
let mut ret = None;
let Some(result) = &state.build else {
return ret;
};
// Header
let available_width = ui.available_width();
render_header(ui, available_width, 2, |ui, column| {
if column == 0 {
// Left column
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.label("Target object");
if result.first_status.success {
if result.first_obj.is_none() {
ui.colored_label(appearance.replace_color, "Missing");
} else {
ui.colored_label(appearance.highlight_color, state.object_name.clone());
}
} else {
ui.colored_label(appearance.delete_color, "Fail");
}
});
let mut search = state.search.clone();
if TextEdit::singleline(&mut search).hint_text("Filter symbols").ui(ui).changed() {
ret = Some(DiffViewAction::SetSearch(search));
}
} else if column == 1 {
// Right column
ui.horizontal(|ui| {
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.label("Base object");
});
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.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
if result.second_status.success {
if result.second_obj.is_none() {
ui.colored_label(appearance.replace_color, "Missing");
} else {
ui.colored_label(appearance.highlight_color, "OK");
}
} else {
ui.colored_label(appearance.delete_color, "Fail");
}
});
if ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() {
ret = Some(DiffViewAction::Build);
}
}
});
// Table
let filter = match &state.search_regex {
Some(regex) => SymbolFilter::Search(regex),
_ => SymbolFilter::None,
};
render_strips(ui, available_width, 2, |ui, column| {
if column == 0 {
// Left column
if result.first_status.success {
if let Some((obj, diff)) = &result.first_obj {
if let Some(result) = symbol_list_ui(
ui,
SymbolDiffContext { obj, diff },
result
.second_obj
.as_ref()
.map(|(obj, diff)| SymbolDiffContext { obj, diff }),
&state.symbol_state,
filter,
appearance,
column,
) {
ret = Some(result);
}
} else {
missing_obj_ui(ui, appearance);
}
} else {
build_log_ui(ui, &result.first_status, appearance);
}
} else if column == 1 {
// Right column
if result.second_status.success {
if let Some((obj, diff)) = &result.second_obj {
if let Some(result) = symbol_list_ui(
ui,
SymbolDiffContext { obj, diff },
result
.first_obj
.as_ref()
.map(|(obj, diff)| SymbolDiffContext { obj, diff }),
&state.symbol_state,
filter,
appearance,
column,
) {
ret = Some(result);
}
} else {
missing_obj_ui(ui, appearance);
}
} else {
build_log_ui(ui, &result.second_status, appearance);
}
}
});
ret
}

4
objdiff-wasm/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
dist/
gen/
node_modules/
pkg/

View File

@@ -0,0 +1,28 @@
import globals from "globals";
import pluginJs from "@eslint/js";
import tseslint from "typescript-eslint";
export default [
{files: ["**/*.{js,mjs,cjs,ts}"]},
{languageOptions: {globals: globals.browser}},
pluginJs.configs.recommended,
...tseslint.configs.recommended,
{
rules: {
"semi": [2, "always"],
"@typescript-eslint/no-unused-vars": [
"error",
// https://typescript-eslint.io/rules/no-unused-vars/#benefits-over-typescript
{
"args": "all",
"argsIgnorePattern": "^_",
"caughtErrors": "all",
"caughtErrorsIgnorePattern": "^_",
"destructuredArrayIgnorePattern": "^_",
"varsIgnorePattern": "^_",
"ignoreRestSiblings": true
},
],
}
},
];

3519
objdiff-wasm/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

39
objdiff-wasm/package.json Normal file
View File

@@ -0,0 +1,39 @@
{
"name": "objdiff-wasm",
"version": "2.0.0",
"description": "A local diffing tool for decompilation projects.",
"author": {
"name": "Luke Street",
"email": "luke@street.dev"
},
"license": "MIT OR Apache-2.0",
"type": "module",
"repository": {
"type": "git",
"url": "git+https://github.com/encounter/objdiff.git"
},
"files": [
"dist/*"
],
"main": "dist/main.js",
"types": "dist/main.d.ts",
"scripts": {
"build": "tsup",
"build:all": "npm run build:wasm && npm run build:proto && npm run build",
"build:proto": "protoc --ts_out=gen --ts_opt add_pb_suffix,eslint_disable,ts_nocheck,use_proto_field_name --proto_path=../objdiff-core/protos ../objdiff-core/protos/*.proto",
"build:wasm": "cd ../objdiff-core && wasm-pack build --out-dir ../objdiff-wasm/pkg --target web -- --features arm,dwarf,ppc,x86,wasm"
},
"dependencies": {
"@protobuf-ts/runtime": "^2.9.4"
},
"devDependencies": {
"@eslint/js": "^9.9.0",
"@protobuf-ts/plugin": "^2.9.4",
"@types/node": "^22.4.1",
"esbuild": "^0.23.1",
"eslint": "^9.9.0",
"globals": "^15.9.0",
"tsup": "^8.2.4",
"typescript-eslint": "^8.2.0"
}
}

227
objdiff-wasm/src/main.ts Normal file
View File

@@ -0,0 +1,227 @@
import {ArgumentValue, DiffResult, InstructionDiff, RelocationTarget} from "../gen/diff_pb";
import type {
ArmArchVersion,
ArmR9Usage,
DiffObjConfig,
MipsAbi,
MipsInstrCategory,
X86Formatter
} from '../pkg';
import {AnyHandlerData, InMessage, OutMessage} from './worker';
// Export wasm types
export {ArmArchVersion, ArmR9Usage, MipsAbi, MipsInstrCategory, X86Formatter, DiffObjConfig};
// Export protobuf types
export * from '../gen/diff_pb';
interface PromiseCallbacks<T> {
start: number;
resolve: (value: T | PromiseLike<T>) => void;
reject: (reason?: string) => void;
}
let workerInit = false;
let workerCallbacks: PromiseCallbacks<Worker>;
const workerReady = new Promise<Worker>((resolve, reject) => {
workerCallbacks = {start: performance.now(), resolve, reject};
});
export async function initialize(data?: {
workerUrl?: string | URL,
wasmUrl?: string | URL, // Relative to worker URL
}): Promise<Worker> {
if (workerInit) {
return workerReady;
}
workerInit = true;
let {workerUrl, wasmUrl} = data || {};
if (!workerUrl) {
try {
// Bundlers will convert this into an asset URL
workerUrl = new URL('./worker.js', import.meta.url);
} catch (_) {
workerUrl = 'worker.js';
}
}
if (!wasmUrl) {
try {
// Bundlers will convert this into an asset URL
wasmUrl = new URL('./objdiff_core_bg.wasm', import.meta.url);
} catch (_) {
wasmUrl = 'objdiff_core_bg.js';
}
}
const worker = new Worker(workerUrl, {
name: 'objdiff',
type: 'module',
});
worker.onmessage = onMessage;
worker.onerror = (event) => {
console.error("Worker error", event);
workerCallbacks.reject("Worker failed to initialize, wrong URL?");
};
defer<void>({
type: 'init',
// URL can't be sent directly
wasmUrl: wasmUrl.toString(),
}, worker).then(() => {
workerCallbacks.resolve(worker);
}, (e) => {
workerCallbacks.reject(e);
});
return workerReady;
}
let globalMessageId = 0;
const messageCallbacks = new Map<number, PromiseCallbacks<never>>();
function onMessage(event: MessageEvent<OutMessage>) {
switch (event.data.type) {
case 'result': {
const {result, error, messageId} = event.data;
const callbacks = messageCallbacks.get(messageId);
if (callbacks) {
const end = performance.now();
console.debug(`Message ${messageId} took ${end - callbacks.start}ms`);
messageCallbacks.delete(messageId);
if (error != null) {
callbacks.reject(error);
} else {
callbacks.resolve(result as never);
}
} else {
console.warn(`Unknown message ID ${messageId}`);
}
break;
}
}
}
async function defer<T>(message: AnyHandlerData, worker?: Worker): Promise<T> {
worker = worker || await initialize();
const messageId = globalMessageId++;
const promise = new Promise<T>((resolve, reject) => {
messageCallbacks.set(messageId, {start: performance.now(), resolve, reject});
});
worker.postMessage({
...message,
messageId
} as InMessage);
return promise;
}
export async function runDiff(left: Uint8Array | undefined, right: Uint8Array | undefined, config?: DiffObjConfig): Promise<DiffResult> {
const data = await defer<Uint8Array>({
type: 'run_diff_proto',
left,
right,
config
});
const parseStart = performance.now();
const result = DiffResult.fromBinary(data, {readUnknownField: false});
const end = performance.now();
console.debug(`Parsing message took ${end - parseStart}ms`);
return result;
}
export type DiffText =
DiffTextBasic
| DiffTextBasicColor
| DiffTextAddress
| DiffTextLine
| DiffTextOpcode
| DiffTextArgument
| DiffTextSymbol
| DiffTextBranchDest
| DiffTextSpacing;
type DiffTextBase = {
diff_index?: number,
};
export type DiffTextBasic = DiffTextBase & {
type: 'basic',
text: string,
};
export type DiffTextBasicColor = DiffTextBase & {
type: 'basic_color',
text: string,
index: number,
};
export type DiffTextAddress = DiffTextBase & {
type: 'address',
address: bigint,
};
export type DiffTextLine = DiffTextBase & {
type: 'line',
line_number: number,
};
export type DiffTextOpcode = DiffTextBase & {
type: 'opcode',
mnemonic: string,
opcode: number,
};
export type DiffTextArgument = DiffTextBase & {
type: 'argument',
value: ArgumentValue,
};
export type DiffTextSymbol = DiffTextBase & {
type: 'symbol',
target: RelocationTarget,
};
export type DiffTextBranchDest = DiffTextBase & {
type: 'branch_dest',
address: bigint,
};
export type DiffTextSpacing = DiffTextBase & {
type: 'spacing',
count: number,
};
// Native JavaScript implementation of objdiff_core::diff::display::display_diff
export function displayDiff(diff: InstructionDiff, baseAddr: bigint, cb: (text: DiffText) => void) {
const ins = diff.instruction;
if (!ins) {
return;
}
if (ins.line_number != null) {
cb({type: 'line', line_number: ins.line_number});
}
cb({type: 'address', address: ins.address - baseAddr});
if (diff.branch_from) {
cb({type: 'basic_color', text: ' ~> ', index: diff.branch_from.branch_index});
} else {
cb({type: 'spacing', count: 4});
}
cb({type: 'opcode', mnemonic: ins.mnemonic, opcode: ins.opcode});
for (let i = 0; i < ins.arguments.length; i++) {
if (i === 0) {
cb({type: 'spacing', count: 1});
}
const arg = ins.arguments[i].value;
const diff_index = diff.arg_diff[i]?.diff_index;
switch (arg.oneofKind) {
case "plain_text":
cb({type: 'basic', text: arg.plain_text, diff_index});
break;
case "argument":
cb({type: 'argument', value: arg.argument, diff_index});
break;
case "relocation": {
const reloc = ins.relocation!;
cb({type: 'symbol', target: reloc.target!, diff_index});
break;
}
case "branch_dest":
if (arg.branch_dest < baseAddr) {
cb({type: 'basic', text: '<unknown>', diff_index});
} else {
cb({type: 'branch_dest', address: arg.branch_dest - baseAddr, diff_index});
}
break;
}
}
if (diff.branch_to) {
cb({type: 'basic_color', text: ' ~> ', index: diff.branch_to.branch_index});
}
}

View File

@@ -0,0 +1,93 @@
import wasmInit, * as exports from '../pkg';
const handlers = {
init: init,
// run_diff_json: run_diff_json,
run_diff_proto: run_diff_proto,
} as const;
type ExtractData<T> = T extends (arg: infer U) => Promise<unknown> ? U : never;
type HandlerData = {
[K in keyof typeof handlers]: { type: K } & ExtractData<typeof handlers[K]>;
};
let wasmReady: Promise<void> | null = null;
async function init({wasmUrl}: { wasmUrl?: string }): Promise<void> {
if (wasmReady != null) {
throw new Error('Already initialized');
}
wasmReady = wasmInit({module_or_path: wasmUrl})
.then(() => {
});
return wasmReady;
}
async function initIfNeeded() {
if (wasmReady == null) {
await init({});
}
return wasmReady;
}
// async function run_diff_json({left, right, config}: {
// left: Uint8Array | undefined,
// right: Uint8Array | undefined,
// config?: exports.DiffObjConfig,
// }): Promise<string> {
// config = config || exports.default_diff_obj_config();
// return exports.run_diff_json(left, right, cfg);
// }
async function run_diff_proto({left, right, config}: {
left: Uint8Array | undefined,
right: Uint8Array | undefined,
config?: exports.DiffObjConfig,
}): Promise<Uint8Array> {
config = config || {};
return exports.run_diff_proto(left, right, config);
}
export type AnyHandlerData = HandlerData[keyof HandlerData];
export type InMessage = AnyHandlerData & { messageId: number };
export type OutMessage = {
type: 'result',
result: unknown | null,
error: string | null,
messageId: number,
};
self.onmessage = (event: MessageEvent<InMessage>) => {
const data = event.data;
const messageId = data?.messageId;
(async () => {
if (!data) {
throw new Error('No data');
}
const handler = handlers[data.type];
if (handler) {
if (data.type !== 'init') {
await initIfNeeded();
}
const start = performance.now();
const result = await handler(data as never);
const end = performance.now();
console.debug(`Worker message ${data.messageId} took ${end - start}ms`);
self.postMessage({
type: 'result',
result: result,
error: null,
messageId,
} as OutMessage);
} else {
throw new Error(`No handler for ${data.type}`);
}
})().catch(error => {
self.postMessage({
type: 'result',
result: null,
error: error.toString(),
messageId,
} as OutMessage);
});
};

View File

@@ -0,0 +1,9 @@
{
"compilerOptions": {
"esModuleInterop": true,
"module": "ES2022",
"moduleResolution": "Node",
"strict": true,
"target": "ES2022",
}
}

View File

@@ -0,0 +1,33 @@
import {defineConfig} from 'tsup';
import fs from 'node:fs/promises';
export default defineConfig([
// Build main library
{
entry: ['src/main.ts'],
clean: true,
dts: true,
format: 'esm',
outDir: 'dist',
skipNodeModulesBundle: true,
sourcemap: true,
splitting: false,
target: 'es2022',
},
// Build web worker
{
entry: ['src/worker.ts'],
clean: true,
dts: true,
format: 'esm', // type: 'module'
minify: true,
outDir: 'dist',
sourcemap: true,
splitting: false,
target: 'es2022',
// https://github.com/egoist/tsup/issues/278
async onSuccess() {
await fs.copyFile('pkg/objdiff_core_bg.wasm', 'dist/objdiff_core_bg.wasm');
}
}
]);

View File

@@ -1,539 +0,0 @@
use std::{
default::Default,
ffi::OsStr,
path::{Path, PathBuf},
rc::Rc,
sync::{
atomic::{AtomicBool, Ordering},
Arc, Mutex, RwLock,
},
time::Duration,
};
use egui::{Color32, FontFamily, FontId, TextStyle};
use notify::{RecursiveMode, Watcher};
use time::{OffsetDateTime, UtcOffset};
use crate::{
jobs::{
check_update::{queue_check_update, CheckUpdateResult},
objdiff::{queue_build, BuildStatus, ObjDiffResult},
Job, JobResult, JobState, JobStatus,
},
views::{
config::config_ui, data_diff::data_diff_ui, function_diff::function_diff_ui, jobs::jobs_ui,
symbol_diff::symbol_diff_ui,
},
};
#[allow(clippy::enum_variant_names)]
#[derive(Default, Eq, PartialEq)]
pub enum View {
#[default]
SymbolDiff,
FunctionDiff,
DataDiff,
}
#[derive(Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub enum DiffKind {
#[default]
SplitObj,
WholeBinary,
}
#[derive(Default, Clone)]
pub struct DiffConfig {
// TODO
// pub stripped_symbols: Vec<String>,
// pub mapped_symbols: HashMap<String, String>,
}
const DEFAULT_COLOR_ROTATION: [Color32; 9] = [
Color32::from_rgb(255, 0, 255),
Color32::from_rgb(0, 255, 255),
Color32::from_rgb(0, 128, 0),
Color32::from_rgb(255, 0, 0),
Color32::from_rgb(255, 255, 0),
Color32::from_rgb(255, 192, 203),
Color32::from_rgb(0, 0, 255),
Color32::from_rgb(0, 255, 0),
Color32::from_rgb(128, 128, 128),
];
#[derive(serde::Deserialize, serde::Serialize)]
#[serde(default)]
pub struct ViewConfig {
pub ui_font: FontId,
pub code_font: FontId,
pub diff_colors: Vec<Color32>,
}
impl Default for ViewConfig {
fn default() -> Self {
Self {
ui_font: FontId { size: 14.0, family: FontFamily::Proportional },
code_font: FontId { size: 14.0, family: FontFamily::Monospace },
diff_colors: DEFAULT_COLOR_ROTATION.to_vec(),
}
}
}
#[derive(serde::Deserialize, serde::Serialize)]
#[serde(default)]
pub struct ViewState {
#[serde(skip)]
pub jobs: Vec<JobState>,
#[serde(skip)]
pub build: Option<Box<ObjDiffResult>>,
#[serde(skip)]
pub highlighted_symbol: Option<String>,
#[serde(skip)]
pub selected_symbol: Option<String>,
#[serde(skip)]
pub current_view: View,
#[serde(skip)]
pub show_config: bool,
#[serde(skip)]
pub show_demangle: bool,
#[serde(skip)]
pub demangle_text: String,
#[serde(skip)]
pub diff_config: DiffConfig,
#[serde(skip)]
pub search: String,
#[serde(skip)]
pub utc_offset: UtcOffset,
#[serde(skip)]
pub check_update: Option<Box<CheckUpdateResult>>,
// Config
pub diff_kind: DiffKind,
pub reverse_fn_order: bool,
pub view_config: ViewConfig,
}
impl Default for ViewState {
fn default() -> Self {
Self {
jobs: vec![],
build: None,
highlighted_symbol: None,
selected_symbol: None,
current_view: Default::default(),
show_config: false,
show_demangle: false,
demangle_text: String::new(),
diff_config: Default::default(),
search: Default::default(),
utc_offset: UtcOffset::UTC,
check_update: None,
diff_kind: Default::default(),
reverse_fn_order: false,
view_config: Default::default(),
}
}
}
#[derive(Default, Clone, serde::Deserialize, serde::Serialize)]
#[serde(default)]
pub struct AppConfig {
pub custom_make: Option<String>,
// WSL2 settings
#[serde(skip)]
pub available_wsl_distros: Option<Vec<String>>,
pub selected_wsl_distro: Option<String>,
// Split obj
pub project_dir: Option<PathBuf>,
pub target_obj_dir: Option<PathBuf>,
pub base_obj_dir: Option<PathBuf>,
pub obj_path: Option<String>,
pub build_target: bool,
// Whole binary
pub left_obj: Option<PathBuf>,
pub right_obj: Option<PathBuf>,
#[serde(skip)]
pub project_dir_change: bool,
#[serde(skip)]
pub queue_update_check: bool,
pub auto_update_check: bool,
}
#[derive(Default, Clone, serde::Deserialize)]
#[serde(default)]
pub struct ProjectConfig {
pub custom_make: Option<String>,
pub project_dir: Option<PathBuf>,
pub target_obj_dir: Option<PathBuf>,
pub base_obj_dir: Option<PathBuf>,
pub build_target: bool,
}
/// We derive Deserialize/Serialize so we can persist app state on shutdown.
#[derive(serde::Deserialize, serde::Serialize)]
#[serde(default)]
pub struct App {
view_state: ViewState,
#[serde(skip)]
config: Arc<RwLock<AppConfig>>,
#[serde(skip)]
modified: Arc<AtomicBool>,
#[serde(skip)]
watcher: Option<notify::RecommendedWatcher>,
#[serde(skip)]
relaunch_path: Rc<Mutex<Option<PathBuf>>>,
#[serde(skip)]
should_relaunch: bool,
}
impl Default for App {
fn default() -> Self {
Self {
view_state: ViewState::default(),
config: Arc::new(Default::default()),
modified: Arc::new(Default::default()),
watcher: None,
relaunch_path: Default::default(),
should_relaunch: false,
}
}
}
const CONFIG_KEY: &str = "app_config";
impl App {
/// Called once before the first frame.
pub fn new(
cc: &eframe::CreationContext<'_>,
utc_offset: UtcOffset,
relaunch_path: Rc<Mutex<Option<PathBuf>>>,
) -> Self {
// This is also where you can customized the look at feel of egui using
// `cc.egui_ctx.set_visuals` and `cc.egui_ctx.set_fonts`.
// Load previous app state (if any).
// Note that you must enable the `persistence` feature for this to work.
if let Some(storage) = cc.storage {
let mut app: App = eframe::get_value(storage, eframe::APP_KEY).unwrap_or_default();
let mut config: AppConfig = eframe::get_value(storage, CONFIG_KEY).unwrap_or_default();
if config.project_dir.is_some() {
config.project_dir_change = true;
}
config.queue_update_check = config.auto_update_check;
app.config = Arc::new(RwLock::new(config));
app.view_state.utc_offset = utc_offset;
app.relaunch_path = relaunch_path;
app
} else {
let mut app = Self::default();
app.view_state.utc_offset = utc_offset;
app.relaunch_path = relaunch_path;
app
}
}
}
impl eframe::App for App {
/// Called each time the UI needs repainting, which may be many times per second.
/// Put your widgets into a `SidePanel`, `TopPanel`, `CentralPanel`, `Window` or `Area`.
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
if self.should_relaunch {
frame.close();
return;
}
let Self { config, view_state, .. } = self;
{
let config = &view_state.view_config;
let mut style = (*ctx.style()).clone();
style.text_styles.insert(TextStyle::Body, FontId {
size: (config.ui_font.size * 0.75).floor(),
family: config.ui_font.family.clone(),
});
style.text_styles.insert(TextStyle::Body, config.ui_font.clone());
style.text_styles.insert(TextStyle::Button, config.ui_font.clone());
style.text_styles.insert(TextStyle::Heading, FontId {
size: (config.ui_font.size * 1.5).floor(),
family: config.ui_font.family.clone(),
});
style.text_styles.insert(TextStyle::Monospace, config.code_font.clone());
ctx.set_style(style);
}
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
egui::menu::bar(ui, |ui| {
ui.menu_button("File", |ui| {
if ui.button("Show config").clicked() {
view_state.show_config = !view_state.show_config;
}
if ui.button("Quit").clicked() {
frame.close();
}
});
ui.menu_button("Tools", |ui| {
if ui.button("Demangle").clicked() {
view_state.show_demangle = !view_state.show_demangle;
}
});
});
});
if view_state.current_view == View::FunctionDiff
&& matches!(&view_state.build, Some(b) if b.first_status.success && b.second_status.success)
{
// egui::SidePanel::left("side_panel").show(ctx, |ui| {
// if ui.button("Back").clicked() {
// view_state.current_view = View::SymbolDiff;
// }
// ui.separator();
// jobs_ui(ui, view_state);
// });
egui::CentralPanel::default().show(ctx, |ui| {
if function_diff_ui(ui, view_state) {
view_state
.jobs
.push(queue_build(config.clone(), view_state.diff_config.clone()));
}
});
} else if view_state.current_view == View::DataDiff
&& matches!(&view_state.build, Some(b) if b.first_status.success && b.second_status.success)
{
egui::CentralPanel::default().show(ctx, |ui| {
if data_diff_ui(ui, view_state) {
view_state
.jobs
.push(queue_build(config.clone(), view_state.diff_config.clone()));
}
});
} else {
egui::SidePanel::left("side_panel").show(ctx, |ui| {
config_ui(ui, config, view_state);
jobs_ui(ui, view_state);
});
egui::CentralPanel::default().show(ctx, |ui| {
symbol_diff_ui(ui, view_state);
});
}
egui::Window::new("Config").open(&mut view_state.show_config).show(ctx, |ui| {
ui.label("UI font:");
egui::introspection::font_id_ui(ui, &mut view_state.view_config.ui_font);
ui.separator();
ui.label("Code font:");
egui::introspection::font_id_ui(ui, &mut view_state.view_config.code_font);
ui.separator();
ui.label("Diff colors:");
if ui.button("Reset").clicked() {
view_state.view_config.diff_colors = DEFAULT_COLOR_ROTATION.to_vec();
}
let mut remove_at: Option<usize> = None;
let num_colors = view_state.view_config.diff_colors.len();
for (idx, color) in view_state.view_config.diff_colors.iter_mut().enumerate() {
ui.horizontal(|ui| {
ui.color_edit_button_srgba(color);
if num_colors > 1 && ui.small_button("-").clicked() {
remove_at = Some(idx);
}
});
}
if let Some(idx) = remove_at {
view_state.view_config.diff_colors.remove(idx);
}
if ui.small_button("+").clicked() {
view_state.view_config.diff_colors.push(Color32::BLACK);
}
});
egui::Window::new("Demangle").open(&mut view_state.show_demangle).show(ctx, |ui| {
ui.text_edit_singleline(&mut view_state.demangle_text);
ui.add_space(10.0);
if let Some(demangled) =
cwdemangle::demangle(&view_state.demangle_text, &Default::default())
{
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(TextStyle::Monospace);
ui.colored_label(Color32::LIGHT_BLUE, &demangled);
});
if ui.button("Copy").clicked() {
ui.output().copied_text = demangled;
}
} else {
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(TextStyle::Monospace);
ui.colored_label(Color32::LIGHT_RED, "[invalid]");
});
}
});
// Windows + request_repaint_after breaks dialogs:
// https://github.com/emilk/egui/issues/2003
if cfg!(windows)
|| view_state.jobs.iter().any(|job| {
if let Some(handle) = &job.handle {
return !handle.is_finished();
}
false
})
{
ctx.request_repaint();
} else {
ctx.request_repaint_after(Duration::from_millis(100));
}
}
/// Called by the frame work to save state before shutdown.
fn save(&mut self, storage: &mut dyn eframe::Storage) {
if let Ok(config) = self.config.read() {
eframe::set_value(storage, CONFIG_KEY, &*config);
}
eframe::set_value(storage, eframe::APP_KEY, self);
}
fn post_rendering(&mut self, _window_size_px: [u32; 2], _frame: &eframe::Frame) {
for job in &mut self.view_state.jobs {
if let Some(handle) = &job.handle {
if !handle.is_finished() {
continue;
}
match job.handle.take().unwrap().join() {
Ok(result) => {
log::info!("Job {} finished", job.id);
match result {
JobResult::None => {
if let Some(err) = &job.status.read().unwrap().error {
log::error!("{:?}", err);
}
}
JobResult::ObjDiff(state) => {
self.view_state.build = Some(state);
}
JobResult::BinDiff(state) => {
self.view_state.build = Some(Box::new(ObjDiffResult {
first_status: BuildStatus {
success: true,
log: "".to_string(),
},
second_status: BuildStatus {
success: true,
log: "".to_string(),
},
first_obj: Some(state.first_obj),
second_obj: Some(state.second_obj),
time: OffsetDateTime::now_utc(),
}));
}
JobResult::CheckUpdate(state) => {
self.view_state.check_update = Some(state);
}
JobResult::Update(state) => {
if let Ok(mut guard) = self.relaunch_path.lock() {
*guard = Some(state.exe_path);
}
self.should_relaunch = true;
}
}
}
Err(err) => {
let err = if let Some(msg) = err.downcast_ref::<&'static str>() {
anyhow::Error::msg(*msg)
} else if let Some(msg) = err.downcast_ref::<String>() {
anyhow::Error::msg(msg.clone())
} else {
anyhow::Error::msg("Thread panicked")
};
let result = job.status.write();
if let Ok(mut guard) = result {
guard.error = Some(err);
} else {
drop(result);
job.status = Arc::new(RwLock::new(JobStatus {
title: "Error".to_string(),
progress_percent: 0.0,
progress_items: None,
status: "".to_string(),
error: Some(err),
}));
}
}
}
}
}
if self.view_state.jobs.iter().any(|v| v.should_remove) {
let mut i = 0;
while i < self.view_state.jobs.len() {
let job = &self.view_state.jobs[i];
if job.should_remove
&& job.handle.is_none()
&& job.status.read().unwrap().error.is_none()
{
self.view_state.jobs.remove(i);
} else {
i += 1;
}
}
}
if let Ok(mut config) = self.config.write() {
if config.project_dir_change {
drop(self.watcher.take());
if let Some(project_dir) = &config.project_dir {
match create_watcher(self.modified.clone(), project_dir) {
Ok(watcher) => self.watcher = Some(watcher),
Err(e) => eprintln!("Failed to create watcher: {e}"),
}
config.project_dir_change = false;
self.modified.store(true, Ordering::Relaxed);
}
}
if config.obj_path.is_some() && self.modified.load(Ordering::Relaxed) {
if !self
.view_state
.jobs
.iter()
.any(|j| j.job_type == Job::ObjDiff && j.handle.is_some())
{
self.view_state.jobs.push(queue_build(
self.config.clone(),
self.view_state.diff_config.clone(),
));
}
self.modified.store(false, Ordering::Relaxed);
}
if config.queue_update_check {
self.view_state.jobs.push(queue_check_update());
config.queue_update_check = false;
}
}
}
}
fn create_watcher(
modified: Arc<AtomicBool>,
project_dir: &Path,
) -> notify::Result<notify::RecommendedWatcher> {
let mut watcher =
notify::recommended_watcher(move |res: notify::Result<notify::Event>| match res {
Ok(event) => {
if matches!(event.kind, notify::EventKind::Modify(..)) {
let watch_extensions = &[
Some(OsStr::new("c")),
Some(OsStr::new("cp")),
Some(OsStr::new("cpp")),
Some(OsStr::new("h")),
Some(OsStr::new("hpp")),
Some(OsStr::new("s")),
];
if event.paths.iter().any(|p| watch_extensions.contains(&p.extension())) {
modified.store(true, Ordering::Relaxed);
}
}
}
Err(e) => println!("watch error: {e:?}"),
})?;
watcher.watch(project_dir, RecursiveMode::Recursive)?;
Ok(watcher)
}

View File

@@ -1,691 +0,0 @@
use std::{collections::BTreeMap, mem::take};
use anyhow::Result;
use crate::{
app::DiffConfig,
editops::{editops_find, LevEditType},
obj::{
mips, ppc, ObjArchitecture, ObjDataDiff, ObjDataDiffKind, ObjInfo, ObjInsArg,
ObjInsArgDiff, ObjInsBranchFrom, ObjInsBranchTo, ObjInsDiff, ObjInsDiffKind, ObjReloc,
ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlags,
},
};
fn no_diff_code(
arch: ObjArchitecture,
data: &[u8],
symbol: &mut ObjSymbol,
relocs: &[ObjReloc],
) -> Result<()> {
let code =
&data[symbol.section_address as usize..(symbol.section_address + symbol.size) as usize];
let (_, ins) = match arch {
ObjArchitecture::PowerPc => ppc::process_code(code, symbol.address, relocs)?,
ObjArchitecture::Mips => {
mips::process_code(code, symbol.address, symbol.address + symbol.size, relocs)?
}
};
let mut diff = Vec::<ObjInsDiff>::new();
for i in ins {
diff.push(ObjInsDiff { ins: Some(i), kind: ObjInsDiffKind::None, ..Default::default() });
}
resolve_branches(&mut diff);
symbol.instructions = diff;
Ok(())
}
pub fn diff_code(
arch: ObjArchitecture,
left_data: &[u8],
right_data: &[u8],
left_symbol: &mut ObjSymbol,
right_symbol: &mut ObjSymbol,
left_relocs: &[ObjReloc],
right_relocs: &[ObjReloc],
) -> Result<()> {
let left_code = &left_data[left_symbol.section_address as usize
..(left_symbol.section_address + left_symbol.size) as usize];
let right_code = &right_data[right_symbol.section_address as usize
..(right_symbol.section_address + right_symbol.size) as usize];
let ((left_ops, left_insts), (right_ops, right_insts)) = match arch {
ObjArchitecture::PowerPc => (
ppc::process_code(left_code, left_symbol.address, left_relocs)?,
ppc::process_code(right_code, right_symbol.address, right_relocs)?,
),
ObjArchitecture::Mips => (
mips::process_code(
left_code,
left_symbol.address,
left_symbol.address + left_symbol.size,
left_relocs,
)?,
mips::process_code(
right_code,
right_symbol.address,
left_symbol.address + left_symbol.size,
right_relocs,
)?,
),
};
let mut left_diff = Vec::<ObjInsDiff>::new();
let mut right_diff = Vec::<ObjInsDiff>::new();
let edit_ops = editops_find(&left_ops, &right_ops);
{
let mut op_iter = edit_ops.iter();
let mut left_iter = left_insts.iter();
let mut right_iter = right_insts.iter();
let mut cur_op = op_iter.next();
let mut cur_left = left_iter.next();
let mut cur_right = right_iter.next();
while let Some(op) = cur_op {
let left_addr = op.first_start as u32 * 4;
let right_addr = op.second_start as u32 * 4;
while let (Some(left), Some(right)) = (cur_left, cur_right) {
if (left.address - left_symbol.address as u32) < left_addr {
left_diff.push(ObjInsDiff { ins: Some(left.clone()), ..ObjInsDiff::default() });
right_diff
.push(ObjInsDiff { ins: Some(right.clone()), ..ObjInsDiff::default() });
} else {
break;
}
cur_left = left_iter.next();
cur_right = right_iter.next();
}
if let (Some(left), Some(right)) = (cur_left, cur_right) {
if (left.address - left_symbol.address as u32) != left_addr {
return Err(anyhow::Error::msg("Instruction address mismatch (left)"));
}
if (right.address - right_symbol.address as u32) != right_addr {
return Err(anyhow::Error::msg("Instruction address mismatch (right)"));
}
match op.op_type {
LevEditType::Replace => {
left_diff
.push(ObjInsDiff { ins: Some(left.clone()), ..ObjInsDiff::default() });
right_diff
.push(ObjInsDiff { ins: Some(right.clone()), ..ObjInsDiff::default() });
cur_left = left_iter.next();
cur_right = right_iter.next();
}
LevEditType::Insert => {
left_diff.push(ObjInsDiff::default());
right_diff
.push(ObjInsDiff { ins: Some(right.clone()), ..ObjInsDiff::default() });
cur_right = right_iter.next();
}
LevEditType::Delete => {
left_diff
.push(ObjInsDiff { ins: Some(left.clone()), ..ObjInsDiff::default() });
right_diff.push(ObjInsDiff::default());
cur_left = left_iter.next();
}
LevEditType::Keep => unreachable!(),
}
} else {
break;
}
cur_op = op_iter.next();
}
// Finalize
while cur_left.is_some() || cur_right.is_some() {
left_diff.push(ObjInsDiff { ins: cur_left.cloned(), ..ObjInsDiff::default() });
right_diff.push(ObjInsDiff { ins: cur_right.cloned(), ..ObjInsDiff::default() });
cur_left = left_iter.next();
cur_right = right_iter.next();
}
}
resolve_branches(&mut left_diff);
resolve_branches(&mut right_diff);
let mut diff_state = InsDiffState::default();
for (left, right) in left_diff.iter_mut().zip(right_diff.iter_mut()) {
let result = compare_ins(left, right, &mut diff_state)?;
left.kind = result.kind;
right.kind = result.kind;
left.arg_diff = result.left_args_diff;
right.arg_diff = result.right_args_diff;
}
let total = left_insts.len();
let percent = if diff_state.diff_count >= total {
0.0
} else {
((total - diff_state.diff_count) as f32 / total as f32) * 100.0
};
left_symbol.match_percent = Some(percent);
right_symbol.match_percent = Some(percent);
left_symbol.instructions = left_diff;
right_symbol.instructions = right_diff;
Ok(())
}
fn resolve_branches(vec: &mut [ObjInsDiff]) {
let mut branch_idx = 0usize;
// Map addresses to indices
let mut addr_map = BTreeMap::<u32, usize>::new();
for (i, ins_diff) in vec.iter().enumerate() {
if let Some(ins) = &ins_diff.ins {
addr_map.insert(ins.address, i);
}
}
// Generate branches
let mut branches = BTreeMap::<usize, ObjInsBranchFrom>::new();
for (i, ins_diff) in vec.iter_mut().enumerate() {
if let Some(ins) = &ins_diff.ins {
// if ins.ins.is_blr() || ins.reloc.is_some() {
// continue;
// }
if let Some(ins_idx) = ins
.args
.iter()
.find_map(|a| if let ObjInsArg::BranchOffset(offs) = a { Some(offs) } else { None })
.and_then(|offs| addr_map.get(&((ins.address as i32 + offs) as u32)))
{
if let Some(branch) = branches.get_mut(ins_idx) {
ins_diff.branch_to =
Some(ObjInsBranchTo { ins_idx: *ins_idx, branch_idx: branch.branch_idx });
branch.ins_idx.push(i);
} else {
ins_diff.branch_to = Some(ObjInsBranchTo { ins_idx: *ins_idx, branch_idx });
branches.insert(*ins_idx, ObjInsBranchFrom { ins_idx: vec![i], branch_idx });
branch_idx += 1;
}
}
}
}
// Store branch from
for (i, branch) in branches {
vec[i].branch_from = Some(branch);
}
}
fn address_eq(left: &ObjSymbol, right: &ObjSymbol) -> bool {
left.address as i64 + left.addend == right.address as i64 + right.addend
}
fn reloc_eq(left_reloc: Option<&ObjReloc>, right_reloc: Option<&ObjReloc>) -> bool {
if let (Some(left), Some(right)) = (left_reloc, right_reloc) {
if left.kind != right.kind {
return false;
}
let name_matches = left.target.name == right.target.name;
match (&left.target_section, &right.target_section) {
(Some(sl), Some(sr)) => {
// Match if section and name or address match
sl == sr && (name_matches || address_eq(&left.target, &right.target))
}
(Some(_), None) => false,
(None, Some(_)) => {
// Match if possibly stripped weak symbol
name_matches && right.target.flags.0.contains(ObjSymbolFlags::Weak)
}
(None, None) => name_matches,
}
} else {
false
}
}
fn arg_eq(
left: &ObjInsArg,
right: &ObjInsArg,
left_diff: &ObjInsDiff,
right_diff: &ObjInsDiff,
) -> bool {
return match left {
ObjInsArg::PpcArg(l) => match right {
ObjInsArg::PpcArg(r) => format!("{l}") == format!("{r}"),
_ => false,
},
ObjInsArg::Reloc => {
matches!(right, ObjInsArg::Reloc)
&& reloc_eq(
left_diff.ins.as_ref().and_then(|i| i.reloc.as_ref()),
right_diff.ins.as_ref().and_then(|i| i.reloc.as_ref()),
)
}
ObjInsArg::RelocWithBase => {
matches!(right, ObjInsArg::RelocWithBase)
&& reloc_eq(
left_diff.ins.as_ref().and_then(|i| i.reloc.as_ref()),
right_diff.ins.as_ref().and_then(|i| i.reloc.as_ref()),
)
}
ObjInsArg::MipsArg(ls) => {
matches!(right, ObjInsArg::MipsArg(rs) if ls == rs)
}
ObjInsArg::BranchOffset(_) => {
// Compare dest instruction idx after diffing
left_diff.branch_to.as_ref().map(|b| b.ins_idx)
== right_diff.branch_to.as_ref().map(|b| b.ins_idx)
}
};
}
#[derive(Default)]
struct InsDiffState {
diff_count: usize,
left_arg_idx: usize,
right_arg_idx: usize,
left_args_idx: BTreeMap<String, usize>,
right_args_idx: BTreeMap<String, usize>,
}
#[derive(Default)]
struct InsDiffResult {
kind: ObjInsDiffKind,
left_args_diff: Vec<Option<ObjInsArgDiff>>,
right_args_diff: Vec<Option<ObjInsArgDiff>>,
}
fn compare_ins(
left: &ObjInsDiff,
right: &ObjInsDiff,
state: &mut InsDiffState,
) -> Result<InsDiffResult> {
let mut result = InsDiffResult::default();
if let (Some(left_ins), Some(right_ins)) = (&left.ins, &right.ins) {
if left_ins.args.len() != right_ins.args.len() || left_ins.op != right_ins.op {
// Totally different op
result.kind = ObjInsDiffKind::Replace;
state.diff_count += 1;
return Ok(result);
}
if left_ins.mnemonic != right_ins.mnemonic {
// Same op but different mnemonic, still cmp args
result.kind = ObjInsDiffKind::OpMismatch;
state.diff_count += 1;
}
for (a, b) in left_ins.args.iter().zip(&right_ins.args) {
if arg_eq(a, b, left, right) {
result.left_args_diff.push(None);
result.right_args_diff.push(None);
} else {
if result.kind == ObjInsDiffKind::None {
result.kind = ObjInsDiffKind::ArgMismatch;
state.diff_count += 1;
}
let a_str = match a {
ObjInsArg::PpcArg(arg) => format!("{arg}"),
ObjInsArg::Reloc | ObjInsArg::RelocWithBase => String::new(),
ObjInsArg::MipsArg(str) => str.clone(),
ObjInsArg::BranchOffset(arg) => format!("{arg}"),
};
let a_diff = if let Some(idx) = state.left_args_idx.get(&a_str) {
ObjInsArgDiff { idx: *idx }
} else {
let idx = state.left_arg_idx;
state.left_args_idx.insert(a_str, idx);
state.left_arg_idx += 1;
ObjInsArgDiff { idx }
};
let b_str = match b {
ObjInsArg::PpcArg(arg) => format!("{arg}"),
ObjInsArg::Reloc | ObjInsArg::RelocWithBase => String::new(),
ObjInsArg::MipsArg(str) => str.clone(),
ObjInsArg::BranchOffset(arg) => format!("{arg}"),
};
let b_diff = if let Some(idx) = state.right_args_idx.get(&b_str) {
ObjInsArgDiff { idx: *idx }
} else {
let idx = state.right_arg_idx;
state.right_args_idx.insert(b_str, idx);
state.right_arg_idx += 1;
ObjInsArgDiff { idx }
};
result.left_args_diff.push(Some(a_diff));
result.right_args_diff.push(Some(b_diff));
}
}
} else if left.ins.is_some() {
result.kind = ObjInsDiffKind::Delete;
state.diff_count += 1;
} else {
result.kind = ObjInsDiffKind::Insert;
state.diff_count += 1;
}
Ok(result)
}
fn find_section<'a>(obj: &'a mut ObjInfo, name: &str) -> Option<&'a mut ObjSection> {
obj.sections.iter_mut().find(|s| s.name == name)
}
fn find_symbol<'a>(symbols: &'a mut [ObjSymbol], name: &str) -> Option<&'a mut ObjSymbol> {
symbols.iter_mut().find(|s| s.name == name)
}
pub fn diff_objs(left: &mut ObjInfo, right: &mut ObjInfo, _diff_config: &DiffConfig) -> Result<()> {
for left_section in &mut left.sections {
if let Some(right_section) = find_section(right, &left_section.name) {
if left_section.kind == ObjSectionKind::Code {
for left_symbol in &mut left_section.symbols {
if let Some(right_symbol) =
find_symbol(&mut right_section.symbols, &left_symbol.name)
{
left_symbol.diff_symbol = Some(right_symbol.name.clone());
right_symbol.diff_symbol = Some(left_symbol.name.clone());
diff_code(
left.architecture,
&left_section.data,
&right_section.data,
left_symbol,
right_symbol,
&left_section.relocations,
&right_section.relocations,
)?;
} else {
no_diff_code(
left.architecture,
&left_section.data,
left_symbol,
&left_section.relocations,
)?;
}
}
for right_symbol in &mut right_section.symbols {
if right_symbol.instructions.is_empty() {
no_diff_code(
left.architecture,
&right_section.data,
right_symbol,
&right_section.relocations,
)?;
}
}
} else if left_section.kind == ObjSectionKind::Data {
diff_data(left_section, right_section);
// diff_data_symbols(left_section, right_section)?;
} else if left_section.kind == ObjSectionKind::Bss {
diff_bss_symbols(&mut left_section.symbols, &mut right_section.symbols)?;
}
}
}
diff_bss_symbols(&mut left.common, &mut right.common)?;
Ok(())
}
fn diff_bss_symbols(left_symbols: &mut [ObjSymbol], right_symbols: &mut [ObjSymbol]) -> Result<()> {
for left_symbol in left_symbols {
if let Some(right_symbol) = find_symbol(right_symbols, &left_symbol.name) {
left_symbol.diff_symbol = Some(right_symbol.name.clone());
right_symbol.diff_symbol = Some(left_symbol.name.clone());
let percent = if left_symbol.size == right_symbol.size { 100.0 } else { 50.0 };
left_symbol.match_percent = Some(percent);
right_symbol.match_percent = Some(percent);
}
}
Ok(())
}
// WIP diff-by-symbol
#[allow(dead_code)]
fn diff_data_symbols(left: &mut ObjSection, right: &mut ObjSection) -> Result<()> {
let mut left_ops = Vec::<u32>::with_capacity(left.symbols.len());
let mut right_ops = Vec::<u32>::with_capacity(right.symbols.len());
for left_symbol in &left.symbols {
let data = &left.data
[left_symbol.address as usize..(left_symbol.address + left_symbol.size) as usize];
let hash = twox_hash::xxh3::hash64(data);
left_ops.push(hash as u32);
}
for symbol in &right.symbols {
let data = &right.data[symbol.address as usize..(symbol.address + symbol.size) as usize];
let hash = twox_hash::xxh3::hash64(data);
right_ops.push(hash as u32);
}
let edit_ops = editops_find(&left_ops, &right_ops);
if edit_ops.is_empty() && !left.data.is_empty() {
let mut left_iter = left.symbols.iter_mut();
let mut right_iter = right.symbols.iter_mut();
loop {
let (left_symbol, right_symbol) = match (left_iter.next(), right_iter.next()) {
(Some(l), Some(r)) => (l, r),
(None, None) => break,
_ => return Err(anyhow::Error::msg("L/R mismatch in diff_data_symbols")),
};
let left_data = &left.data
[left_symbol.address as usize..(left_symbol.address + left_symbol.size) as usize];
let right_data = &right.data[right_symbol.address as usize
..(right_symbol.address + right_symbol.size) as usize];
left.data_diff.push(ObjDataDiff {
data: left_data.to_vec(),
kind: ObjDataDiffKind::None,
len: left_symbol.size as usize,
symbol: left_symbol.name.clone(),
});
right.data_diff.push(ObjDataDiff {
data: right_data.to_vec(),
kind: ObjDataDiffKind::None,
len: right_symbol.size as usize,
symbol: right_symbol.name.clone(),
});
left_symbol.diff_symbol = Some(right_symbol.name.clone());
left_symbol.match_percent = Some(100.0);
right_symbol.diff_symbol = Some(left_symbol.name.clone());
right_symbol.match_percent = Some(100.0);
}
return Ok(());
}
Ok(())
}
fn diff_data(left: &mut ObjSection, right: &mut ObjSection) {
let edit_ops = editops_find(&left.data, &right.data);
if edit_ops.is_empty() && !left.data.is_empty() {
left.data_diff = vec![ObjDataDiff {
data: left.data.clone(),
kind: ObjDataDiffKind::None,
len: left.data.len(),
symbol: String::new(),
}];
right.data_diff = vec![ObjDataDiff {
data: right.data.clone(),
kind: ObjDataDiffKind::None,
len: right.data.len(),
symbol: String::new(),
}];
return;
}
let mut left_diff = Vec::<ObjDataDiff>::new();
let mut right_diff = Vec::<ObjDataDiff>::new();
let mut left_cur = 0usize;
let mut right_cur = 0usize;
let mut cur_op = LevEditType::Keep;
let mut cur_left_data = Vec::<u8>::new();
let mut cur_right_data = Vec::<u8>::new();
for op in edit_ops {
if cur_op != op.op_type || left_cur < op.first_start || right_cur < op.second_start {
match cur_op {
LevEditType::Keep => {}
LevEditType::Replace => {
let left_data = take(&mut cur_left_data);
let right_data = take(&mut cur_right_data);
let left_data_len = left_data.len();
let right_data_len = right_data.len();
left_diff.push(ObjDataDiff {
data: left_data,
kind: ObjDataDiffKind::Replace,
len: left_data_len,
symbol: String::new(),
});
right_diff.push(ObjDataDiff {
data: right_data,
kind: ObjDataDiffKind::Replace,
len: right_data_len,
symbol: String::new(),
});
}
LevEditType::Insert => {
let right_data = take(&mut cur_right_data);
let right_data_len = right_data.len();
left_diff.push(ObjDataDiff {
data: vec![],
kind: ObjDataDiffKind::Insert,
len: right_data_len,
symbol: String::new(),
});
right_diff.push(ObjDataDiff {
data: right_data,
kind: ObjDataDiffKind::Insert,
len: right_data_len,
symbol: String::new(),
});
}
LevEditType::Delete => {
let left_data = take(&mut cur_left_data);
let left_data_len = left_data.len();
left_diff.push(ObjDataDiff {
data: left_data,
kind: ObjDataDiffKind::Delete,
len: left_data_len,
symbol: String::new(),
});
right_diff.push(ObjDataDiff {
data: vec![],
kind: ObjDataDiffKind::Delete,
len: left_data_len,
symbol: String::new(),
});
}
}
}
if left_cur < op.first_start {
left_diff.push(ObjDataDiff {
data: left.data[left_cur..op.first_start].to_vec(),
kind: ObjDataDiffKind::None,
len: op.first_start - left_cur,
symbol: String::new(),
});
left_cur = op.first_start;
}
if right_cur < op.second_start {
right_diff.push(ObjDataDiff {
data: right.data[right_cur..op.second_start].to_vec(),
kind: ObjDataDiffKind::None,
len: op.second_start - right_cur,
symbol: String::new(),
});
right_cur = op.second_start;
}
match op.op_type {
LevEditType::Replace => {
cur_left_data.push(left.data[left_cur]);
cur_right_data.push(right.data[right_cur]);
left_cur += 1;
right_cur += 1;
}
LevEditType::Insert => {
cur_right_data.push(right.data[right_cur]);
right_cur += 1;
}
LevEditType::Delete => {
cur_left_data.push(left.data[left_cur]);
left_cur += 1;
}
LevEditType::Keep => unreachable!(),
}
cur_op = op.op_type;
}
// if left_cur < left.data.len() {
// let len = left.data.len() - left_cur;
// left_diff.push(ObjDataDiff {
// data: left.data[left_cur..].to_vec(),
// kind: ObjDataDiffKind::Delete,
// len,
// });
// right_diff.push(ObjDataDiff { data: vec![], kind: ObjDataDiffKind::Delete, len });
// } else if right_cur < right.data.len() {
// let len = right.data.len() - right_cur;
// left_diff.push(ObjDataDiff { data: vec![], kind: ObjDataDiffKind::Insert, len });
// right_diff.push(ObjDataDiff {
// data: right.data[right_cur..].to_vec(),
// kind: ObjDataDiffKind::Insert,
// len,
// });
// }
// TODO: merge with above
match cur_op {
LevEditType::Keep => {}
LevEditType::Replace => {
let left_data = take(&mut cur_left_data);
let right_data = take(&mut cur_right_data);
let left_data_len = left_data.len();
let right_data_len = right_data.len();
left_diff.push(ObjDataDiff {
data: left_data,
kind: ObjDataDiffKind::Replace,
len: left_data_len,
symbol: String::new(),
});
right_diff.push(ObjDataDiff {
data: right_data,
kind: ObjDataDiffKind::Replace,
len: right_data_len,
symbol: String::new(),
});
}
LevEditType::Insert => {
let right_data = take(&mut cur_right_data);
let right_data_len = right_data.len();
left_diff.push(ObjDataDiff {
data: vec![],
kind: ObjDataDiffKind::Insert,
len: right_data_len,
symbol: String::new(),
});
right_diff.push(ObjDataDiff {
data: right_data,
kind: ObjDataDiffKind::Insert,
len: right_data_len,
symbol: String::new(),
});
}
LevEditType::Delete => {
let left_data = take(&mut cur_left_data);
let left_data_len = left_data.len();
left_diff.push(ObjDataDiff {
data: left_data,
kind: ObjDataDiffKind::Delete,
len: left_data_len,
symbol: String::new(),
});
right_diff.push(ObjDataDiff {
data: vec![],
kind: ObjDataDiffKind::Delete,
len: left_data_len,
symbol: String::new(),
});
}
}
if left_cur < left.data.len() {
left_diff.push(ObjDataDiff {
data: left.data[left_cur..].to_vec(),
kind: ObjDataDiffKind::None,
len: left.data.len() - left_cur,
symbol: String::new(),
});
}
if right_cur < right.data.len() {
right_diff.push(ObjDataDiff {
data: right.data[right_cur..].to_vec(),
kind: ObjDataDiffKind::None,
len: right.data.len() - right_cur,
symbol: String::new(),
});
}
left.data_diff = left_diff;
right.data_diff = right_diff;
}

View File

@@ -1,258 +0,0 @@
/// Adapted from https://crates.io/crates/rapidfuzz
// Copyright 2020 maxbachmann
//
// Permission is hereby granted, free of charge, to any
// person obtaining a copy of this software and associated
// documentation files (the "Software"), to deal in the
// Software without restriction, including without
// limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software
// is furnished to do so, subject to the following
// conditions:
//
// The above copyright notice and this permission notice
// shall be included in all copies or substantial portions
// of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
// ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
// SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub enum LevEditType {
Keep,
Replace,
Insert,
Delete,
}
#[derive(Debug, PartialEq, Eq)]
pub struct LevEditOp {
pub op_type: LevEditType, /* editing operation type */
pub first_start: usize, /* source block position */
pub second_start: usize, /* destination position */
}
#[derive(Debug, PartialEq, Eq)]
pub struct LevMatchingBlock {
pub first_start: usize,
pub second_start: usize,
pub len: usize,
}
pub fn editops_find<T>(query: &[T], choice: &[T]) -> Vec<LevEditOp>
where T: PartialEq {
let string_affix = Affix::find(query, choice);
let first_string_len = string_affix.first_string_len;
let second_string_len = string_affix.second_string_len;
let prefix_len = string_affix.prefix_len;
let first_string = &query[prefix_len..prefix_len + first_string_len];
let second_string = &choice[prefix_len..prefix_len + second_string_len];
let matrix_columns = first_string_len + 1;
let matrix_rows = second_string_len + 1;
// TODO maybe use an actual matrix for readability
let mut cache_matrix: Vec<usize> = vec![0; matrix_rows * matrix_columns];
for (i, elem) in cache_matrix.iter_mut().enumerate().take(matrix_rows) {
*elem = i;
}
for i in 1..matrix_columns {
cache_matrix[matrix_rows * i] = i;
}
for (i, char1) in first_string.iter().enumerate() {
let mut prev = i * matrix_rows;
let current = prev + matrix_rows;
let mut x = i + 1;
for (p, char2p) in second_string.iter().enumerate() {
let mut c3 = cache_matrix[prev] + (char1 != char2p) as usize;
prev += 1;
x += 1;
if x >= c3 {
x = c3;
}
c3 = cache_matrix[prev] + 1;
if x > c3 {
x = c3;
}
cache_matrix[current + 1 + p] = x;
}
}
editops_from_cost_matrix(
first_string,
second_string,
matrix_columns,
matrix_rows,
prefix_len,
cache_matrix,
)
}
fn editops_from_cost_matrix<T>(
string1: &[T],
string2: &[T],
len1: usize,
len2: usize,
prefix_len: usize,
cache_matrix: Vec<usize>,
) -> Vec<LevEditOp>
where
T: PartialEq,
{
let mut dir = 0;
let mut ops: Vec<LevEditOp> = vec![];
ops.reserve(cache_matrix[len1 * len2 - 1]);
let mut i = len1 - 1;
let mut j = len2 - 1;
let mut p = len1 * len2 - 1;
// let string1_chars: Vec<char> = string1.chars().collect();
// let string2_chars: Vec<char> = string2.chars().collect();
//TODO this is still pretty ugly
while i > 0 || j > 0 {
let current_value = cache_matrix[p];
let op_type;
if dir == -1 && j > 0 && current_value == cache_matrix[p - 1] + 1 {
op_type = LevEditType::Insert;
} else if dir == 1 && i > 0 && current_value == cache_matrix[p - len2] + 1 {
op_type = LevEditType::Delete;
} else if i > 0
&& j > 0
&& current_value == cache_matrix[p - len2 - 1]
&& string1[i - 1] == string2[j - 1]
{
op_type = LevEditType::Keep;
} else if i > 0 && j > 0 && current_value == cache_matrix[p - len2 - 1] + 1 {
op_type = LevEditType::Replace;
}
/* we can't turn directly from -1 to 1, in this case it would be better
* to go diagonally, but check it (dir == 0) */
else if dir == 0 && j > 0 && current_value == cache_matrix[p - 1] + 1 {
op_type = LevEditType::Insert;
} else if dir == 0 && i > 0 && current_value == cache_matrix[p - len2] + 1 {
op_type = LevEditType::Delete;
} else {
panic!("something went terribly wrong");
}
match op_type {
LevEditType::Insert => {
j -= 1;
p -= 1;
dir = -1;
}
LevEditType::Delete => {
i -= 1;
p -= len2;
dir = 1;
}
LevEditType::Replace => {
i -= 1;
j -= 1;
p -= len2 + 1;
dir = 0;
}
LevEditType::Keep => {
i -= 1;
j -= 1;
p -= len2 + 1;
dir = 0;
/* LevEditKeep does not has to be stored */
continue;
}
};
let edit_op =
LevEditOp { op_type, first_start: i + prefix_len, second_start: j + prefix_len };
ops.insert(0, edit_op);
}
ops
}
pub struct Affix {
pub prefix_len: usize,
pub first_string_len: usize,
pub second_string_len: usize,
}
impl Affix {
pub fn find<T>(first_string: &[T], second_string: &[T]) -> Affix
where T: PartialEq {
// remove common prefix and suffix (linear vs square runtime for levensthein)
let mut first_iter = first_string.iter();
let mut second_iter = second_string.iter();
let mut limit_start = 0;
let mut first_iter_char = first_iter.next();
let mut second_iter_char = second_iter.next();
while first_iter_char.is_some() && first_iter_char == second_iter_char {
first_iter_char = first_iter.next();
second_iter_char = second_iter.next();
limit_start += 1;
}
// save char since the iterator was already consumed
let first_iter_cache = first_iter_char;
let second_iter_cache = second_iter_char;
if second_iter_char.is_some() && first_iter_char.is_some() {
first_iter_char = first_iter.next_back();
second_iter_char = second_iter.next_back();
while first_iter_char.is_some() && first_iter_char == second_iter_char {
first_iter_char = first_iter.next_back();
second_iter_char = second_iter.next_back();
}
}
match (first_iter_char, second_iter_char) {
(None, None) => {
// characters might not match even though they were consumed
let remaining_char = (first_iter_cache != second_iter_cache) as usize;
Affix {
prefix_len: limit_start,
first_string_len: remaining_char,
second_string_len: remaining_char,
}
}
(None, _) => {
let remaining_char =
(first_iter_cache.is_some() && first_iter_cache != second_iter_char) as usize;
Affix {
prefix_len: limit_start,
first_string_len: remaining_char,
second_string_len: second_iter.count() + 1 + remaining_char,
}
}
(_, None) => {
let remaining_char =
(second_iter_cache.is_some() && second_iter_cache != first_iter_char) as usize;
Affix {
prefix_len: limit_start,
first_string_len: first_iter.count() + 1 + remaining_char,
second_string_len: remaining_char,
}
}
_ => Affix {
prefix_len: limit_start,
first_string_len: first_iter.count() + 2,
second_string_len: second_iter.count() + 2,
},
}
}
}

View File

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

View File

@@ -1,33 +0,0 @@
use std::sync::mpsc::Receiver;
use anyhow::{Context, Result};
use self_update::{cargo_crate_version, update::Release};
use crate::{
jobs::{queue_job, update_status, Job, JobResult, JobState, Status},
update::{build_updater, BIN_NAME},
};
pub struct CheckUpdateResult {
pub update_available: bool,
pub latest_release: Release,
pub found_binary: bool,
}
fn run_check_update(status: &Status, cancel: Receiver<()>) -> Result<Box<CheckUpdateResult>> {
update_status(status, "Fetching latest release".to_string(), 0, 1, &cancel)?;
let updater = build_updater().context("Failed to create release updater")?;
let latest_release = updater.get_latest_release()?;
let update_available =
self_update::version::bump_is_greater(cargo_crate_version!(), &latest_release.version)?;
let found_binary = latest_release.assets.iter().any(|a| a.name == BIN_NAME);
update_status(status, "Complete".to_string(), 1, 1, &cancel)?;
Ok(Box::new(CheckUpdateResult { update_available, latest_release, found_binary }))
}
pub fn queue_check_update() -> JobState {
queue_job("Check for updates", Job::CheckUpdate, move |status, cancel| {
run_check_update(status, cancel).map(JobResult::CheckUpdate)
})
}

View File

@@ -1,117 +0,0 @@
use std::{
sync::{
atomic::{AtomicUsize, Ordering},
mpsc::{Receiver, Sender, TryRecvError},
Arc, RwLock,
},
thread::JoinHandle,
};
use anyhow::Result;
use crate::jobs::{
bindiff::BinDiffResult, check_update::CheckUpdateResult, objdiff::ObjDiffResult,
update::UpdateResult,
};
pub mod bindiff;
pub mod check_update;
pub mod objdiff;
pub mod update;
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
pub enum Job {
ObjDiff,
BinDiff,
CheckUpdate,
Update,
}
pub static JOB_ID: AtomicUsize = AtomicUsize::new(0);
pub struct JobState {
pub id: usize,
pub job_type: Job,
pub handle: Option<JoinHandle<JobResult>>,
pub status: Arc<RwLock<JobStatus>>,
pub cancel: Sender<()>,
pub should_remove: bool,
}
#[derive(Default)]
pub struct JobStatus {
pub title: String,
pub progress_percent: f32,
pub progress_items: Option<[u32; 2]>,
pub status: String,
pub error: Option<anyhow::Error>,
}
pub enum JobResult {
None,
ObjDiff(Box<ObjDiffResult>),
BinDiff(Box<BinDiffResult>),
CheckUpdate(Box<CheckUpdateResult>),
Update(Box<UpdateResult>),
}
fn should_cancel(rx: &Receiver<()>) -> bool {
match rx.try_recv() {
Ok(_) | Err(TryRecvError::Disconnected) => true,
Err(_) => false,
}
}
type Status = Arc<RwLock<JobStatus>>;
fn queue_job(
title: &str,
job_type: Job,
run: impl FnOnce(&Status, Receiver<()>) -> Result<JobResult> + Send + 'static,
) -> JobState {
let status = Arc::new(RwLock::new(JobStatus {
title: title.to_string(),
progress_percent: 0.0,
progress_items: None,
status: String::new(),
error: None,
}));
let status_clone = status.clone();
let (tx, rx) = std::sync::mpsc::channel();
let handle = std::thread::spawn(move || {
return match run(&status, rx) {
Ok(state) => state,
Err(e) => {
if let Ok(mut w) = status.write() {
w.error = Some(e);
}
JobResult::None
}
};
});
let id = JOB_ID.fetch_add(1, Ordering::Relaxed);
log::info!("Started job {}", id);
JobState {
id,
job_type,
handle: Some(handle),
status: status_clone,
cancel: tx,
should_remove: true,
}
}
fn update_status(
status: &Status,
str: String,
count: u32,
total: u32,
cancel: &Receiver<()>,
) -> Result<()> {
let mut w = status.write().map_err(|_| anyhow::Error::msg("Failed to lock job status"))?;
w.progress_items = Some([count, total]);
w.progress_percent = count as f32 / total as f32;
if should_cancel(cancel) {
w.status = "Cancelled".to_string();
return Err(anyhow::Error::msg("Cancelled"));
} else {
w.status = str;
}
Ok(())
}

View File

@@ -1,143 +0,0 @@
use std::{
path::Path,
process::Command,
str::from_utf8,
sync::{mpsc::Receiver, Arc, RwLock},
};
use anyhow::{Context, Error, Result};
use time::OffsetDateTime;
use crate::{
app::{AppConfig, DiffConfig},
diff::diff_objs,
jobs::{queue_job, update_status, Job, JobResult, JobState, Status},
obj::{elf, ObjInfo},
};
pub struct BuildStatus {
pub success: bool,
pub log: String,
}
pub struct ObjDiffResult {
pub first_status: BuildStatus,
pub second_status: BuildStatus,
pub first_obj: Option<ObjInfo>,
pub second_obj: Option<ObjInfo>,
pub time: OffsetDateTime,
}
fn run_make(cwd: &Path, arg: &Path, config: &AppConfig) -> BuildStatus {
match (|| -> Result<BuildStatus> {
let make = config.custom_make.as_deref().unwrap_or("make");
#[cfg(not(windows))]
let mut command = {
let mut command = Command::new(make);
command.current_dir(cwd).arg(arg);
command
};
#[cfg(windows)]
let mut command = {
use std::os::windows::process::CommandExt;
use path_slash::PathExt;
let mut command = if config.selected_wsl_distro.is_some() {
Command::new("wsl")
} else {
Command::new(make)
};
if let Some(distro) = &config.selected_wsl_distro {
command
.arg("--cd")
.arg(cwd)
.arg("-d")
.arg(distro)
.arg("--")
.arg(make)
.arg(arg.to_slash_lossy().as_ref());
} else {
command.current_dir(cwd).arg(arg.to_slash_lossy().as_ref());
}
command.creation_flags(winapi::um::winbase::CREATE_NO_WINDOW);
command
};
let output = command.output().context("Failed to execute build")?;
let stdout = from_utf8(&output.stdout).context("Failed to process stdout")?;
let stderr = from_utf8(&output.stderr).context("Failed to process stderr")?;
Ok(BuildStatus {
success: output.status.code().unwrap_or(-1) == 0,
log: format!("{stdout}\n{stderr}"),
})
})() {
Ok(status) => status,
Err(e) => BuildStatus { success: false, log: e.to_string() },
}
}
fn run_build(
status: &Status,
cancel: Receiver<()>,
config: Arc<RwLock<AppConfig>>,
diff_config: DiffConfig,
) -> Result<Box<ObjDiffResult>> {
let config = config.read().map_err(|_| Error::msg("Failed to lock app config"))?.clone();
let obj_path = config.obj_path.as_ref().ok_or_else(|| Error::msg("Missing obj path"))?;
let project_dir =
config.project_dir.as_ref().ok_or_else(|| Error::msg("Missing project dir"))?;
let mut target_path = config
.target_obj_dir
.as_ref()
.ok_or_else(|| Error::msg("Missing target obj dir"))?
.to_owned();
target_path.push(obj_path);
let mut base_path =
config.base_obj_dir.as_ref().ok_or_else(|| Error::msg("Missing base obj dir"))?.to_owned();
base_path.push(obj_path);
let target_path_rel = target_path
.strip_prefix(project_dir)
.context("Failed to create relative target obj path")?;
let base_path_rel =
base_path.strip_prefix(project_dir).context("Failed to create relative base obj path")?;
let total = if config.build_target { 5 } else { 4 };
let first_status = if config.build_target {
update_status(status, format!("Building target {obj_path}"), 0, total, &cancel)?;
run_make(project_dir, target_path_rel, &config)
} else {
BuildStatus { success: true, log: String::new() }
};
update_status(status, format!("Building base {obj_path}"), 1, total, &cancel)?;
let second_status = run_make(project_dir, base_path_rel, &config);
let time = OffsetDateTime::now_utc();
let mut first_obj = if first_status.success {
update_status(status, format!("Loading target {obj_path}"), 2, total, &cancel)?;
Some(elf::read(&target_path)?)
} else {
None
};
let mut second_obj = if second_status.success {
update_status(status, format!("Loading base {obj_path}"), 3, total, &cancel)?;
Some(elf::read(&base_path)?)
} else {
None
};
if let (Some(first_obj), Some(second_obj)) = (&mut first_obj, &mut second_obj) {
update_status(status, "Performing diff".to_string(), 4, total, &cancel)?;
diff_objs(first_obj, second_obj, &diff_config)?;
}
update_status(status, "Complete".to_string(), total, total, &cancel)?;
Ok(Box::new(ObjDiffResult { first_status, second_status, first_obj, second_obj, time }))
}
pub fn queue_build(config: Arc<RwLock<AppConfig>>, diff_config: DiffConfig) -> JobState {
queue_job("Object diff", Job::ObjDiff, move |status, cancel| {
run_build(status, cancel, config, diff_config).map(JobResult::ObjDiff)
})
}

View File

@@ -1,11 +0,0 @@
#![warn(clippy::all, rust_2018_idioms)]
pub use app::App;
mod app;
mod diff;
mod editops;
mod jobs;
mod obj;
mod update;
mod views;

View File

@@ -1,70 +0,0 @@
#![warn(clippy::all, rust_2018_idioms)]
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
use std::{path::PathBuf, rc::Rc, sync::Mutex};
use cfg_if::cfg_if;
use time::UtcOffset;
// When compiling natively:
#[cfg(not(target_arch = "wasm32"))]
fn main() {
// Log to stdout (if you run with `RUST_LOG=debug`).
tracing_subscriber::fmt::init();
// Because localtime_r is unsound in multithreaded apps,
// we must call this before initializing eframe.
// https://github.com/time-rs/time/issues/293
let utc_offset = UtcOffset::current_local_offset().unwrap_or(UtcOffset::UTC);
let exec_path: Rc<Mutex<Option<PathBuf>>> = Rc::new(Mutex::new(None));
let exec_path_clone = exec_path.clone();
let native_options = eframe::NativeOptions::default();
// native_options.renderer = eframe::Renderer::Wgpu;
eframe::run_native(
"objdiff",
native_options,
Box::new(move |cc| Box::new(objdiff::App::new(cc, utc_offset, exec_path_clone))),
);
// Attempt to relaunch application from the updated path
if let Ok(mut guard) = exec_path.lock() {
if let Some(path) = guard.take() {
cfg_if! {
if #[cfg(unix)] {
let result = exec::Command::new(path)
.args(&std::env::args().collect::<Vec<String>>())
.exec();
eprintln!("Failed to relaunch: {result:?}");
} else {
let result = std::process::Command::new(path)
.args(std::env::args())
.spawn()
.unwrap()
.wait();
if let Err(e) = result {
eprintln!("Failed to relaunch: {:?}", e);
}
}
}
}
};
}
// when compiling to web using trunk.
#[cfg(target_arch = "wasm32")]
fn main() {
// Make sure panics are logged using `console.error`.
console_error_panic_hook::set_once();
// Redirect tracing to console.log and friends:
tracing_wasm::set_as_global_default();
let web_options = eframe::WebOptions::default();
eframe::start_web(
"the_canvas_id", // hardcode it
web_options,
Box::new(|cc| Box::new(eframe_template::TemplateApp::new(cc))),
)
.expect("failed to start eframe");
}

View File

@@ -1,316 +0,0 @@
use std::{fs, path::Path};
use anyhow::{Context, Result};
use cwdemangle::demangle;
use flagset::Flags;
use object::{
elf::{
R_MIPS_26, R_MIPS_HI16, R_MIPS_LO16, R_PPC_ADDR16_HA, R_PPC_ADDR16_HI, R_PPC_ADDR16_LO,
R_PPC_EMB_SDA21, R_PPC_REL14, R_PPC_REL24,
},
Architecture, File, Object, ObjectSection, ObjectSymbol, RelocationKind, RelocationTarget,
SectionKind, Symbol, SymbolKind, SymbolSection,
};
use crate::obj::{
ObjArchitecture, ObjInfo, ObjReloc, ObjRelocKind, ObjSection, ObjSectionKind, ObjSymbol,
ObjSymbolFlagSet, ObjSymbolFlags,
};
fn to_obj_section_kind(kind: SectionKind) -> ObjSectionKind {
match kind {
SectionKind::Text => ObjSectionKind::Code,
SectionKind::Data | SectionKind::ReadOnlyData => ObjSectionKind::Data,
SectionKind::UninitializedData => ObjSectionKind::Bss,
_ => panic!("Unhandled section kind {kind:?}"),
}
}
fn to_obj_symbol(obj_file: &File<'_>, symbol: &Symbol<'_, '_>, addend: i64) -> Result<ObjSymbol> {
let mut name = symbol.name().context("Failed to process symbol name")?;
if name.is_empty() {
println!("Found empty sym: {symbol:?}");
name = "?";
}
let mut flags = ObjSymbolFlagSet(ObjSymbolFlags::none());
if symbol.is_global() {
flags = ObjSymbolFlagSet(flags.0 | ObjSymbolFlags::Global);
}
if symbol.is_local() {
flags = ObjSymbolFlagSet(flags.0 | ObjSymbolFlags::Local);
}
if symbol.is_common() {
flags = ObjSymbolFlagSet(flags.0 | ObjSymbolFlags::Common);
}
if symbol.is_weak() {
flags = ObjSymbolFlagSet(flags.0 | ObjSymbolFlags::Weak);
}
let section_address = if let Some(section) =
symbol.section_index().and_then(|idx| obj_file.section_by_index(idx).ok())
{
symbol.address() - section.address()
} else {
symbol.address()
};
Ok(ObjSymbol {
name: name.to_string(),
demangled_name: demangle(name, &Default::default()),
address: symbol.address(),
section_address,
size: symbol.size(),
size_known: symbol.size() != 0,
flags,
addend,
diff_symbol: None,
instructions: vec![],
match_percent: None,
})
}
fn filter_sections(obj_file: &File<'_>) -> Result<Vec<ObjSection>> {
let mut result = Vec::<ObjSection>::new();
for section in obj_file.sections() {
if section.size() == 0 {
continue;
}
if section.kind() != SectionKind::Text
&& section.kind() != SectionKind::Data
&& section.kind() != SectionKind::ReadOnlyData
&& section.kind() != SectionKind::UninitializedData
{
continue;
}
let name = section.name().context("Failed to process section name")?;
let data = section.uncompressed_data().context("Failed to read section data")?;
result.push(ObjSection {
name: name.to_string(),
kind: to_obj_section_kind(section.kind()),
address: section.address(),
size: section.size(),
data: data.to_vec(),
index: section.index().0,
symbols: Vec::new(),
relocations: Vec::new(),
data_diff: vec![],
match_percent: 0.0,
});
}
result.sort_by(|a, b| a.name.cmp(&b.name));
Ok(result)
}
fn symbols_by_section(obj_file: &File<'_>, section: &ObjSection) -> Result<Vec<ObjSymbol>> {
let mut result = Vec::<ObjSymbol>::new();
for symbol in obj_file.symbols() {
if symbol.kind() == SymbolKind::Section {
continue;
}
if let Some(index) = symbol.section().index() {
if index.0 == section.index {
if symbol.is_local() && section.kind == ObjSectionKind::Code {
// TODO strip local syms in diff?
let name = symbol.name().context("Failed to process symbol name")?;
if symbol.size() == 0 || name.starts_with("lbl_") {
continue;
}
}
result.push(to_obj_symbol(obj_file, &symbol, 0)?);
}
}
}
result.sort_by_key(|v| v.address);
let mut iter = result.iter_mut().peekable();
while let Some(symbol) = iter.next() {
if symbol.size == 0 {
if let Some(next_symbol) = iter.peek() {
symbol.size = next_symbol.address - symbol.address;
} else {
symbol.size = (section.address + section.size) - symbol.address;
}
}
}
Ok(result)
}
fn common_symbols(obj_file: &File<'_>) -> Result<Vec<ObjSymbol>> {
let mut result = Vec::<ObjSymbol>::new();
for symbol in obj_file.symbols() {
if symbol.is_common() {
result.push(to_obj_symbol(obj_file, &symbol, 0)?);
}
}
Ok(result)
}
fn find_section_symbol(
obj_file: &File<'_>,
target: &Symbol<'_, '_>,
address: u64,
) -> Result<ObjSymbol> {
let section_index =
target.section_index().ok_or_else(|| anyhow::Error::msg("Unknown section index"))?;
let section = obj_file.section_by_index(section_index)?;
let mut closest_symbol: Option<Symbol<'_, '_>> = None;
for symbol in obj_file.symbols() {
if !matches!(symbol.section_index(), Some(idx) if idx == section_index) {
continue;
}
if symbol.kind() == SymbolKind::Section || symbol.address() != address {
if symbol.address() < address
&& symbol.size() != 0
&& (closest_symbol.is_none()
|| matches!(&closest_symbol, Some(s) if s.address() <= symbol.address()))
{
closest_symbol = Some(symbol);
}
continue;
}
return to_obj_symbol(obj_file, &symbol, 0);
}
let (name, offset) = closest_symbol
.and_then(|s| s.name().map(|n| (n, s.address())).ok())
.or_else(|| section.name().map(|n| (n, section.address())).ok())
.unwrap_or(("<unknown>", 0));
let offset_addr = address - offset;
Ok(ObjSymbol {
name: name.to_string(),
demangled_name: None,
address: offset,
section_address: address - section.address(),
size: 0,
size_known: false,
flags: Default::default(),
addend: offset_addr as i64,
diff_symbol: None,
instructions: vec![],
match_percent: None,
})
}
fn relocations_by_section(
arch: ObjArchitecture,
obj_file: &File<'_>,
section: &mut ObjSection,
) -> Result<Vec<ObjReloc>> {
let obj_section = obj_file
.section_by_name(&section.name)
.ok_or_else(|| anyhow::Error::msg("Failed to locate section"))?;
let mut relocations = Vec::<ObjReloc>::new();
for (address, reloc) in obj_section.relocations() {
let symbol = match reloc.target() {
RelocationTarget::Symbol(idx) => obj_file
.symbol_by_index(idx)
.context("Failed to locate relocation target symbol")?,
_ => {
return Err(anyhow::Error::msg(format!(
"Unhandled relocation target: {:?}",
reloc.target()
)));
}
};
let kind = match reloc.kind() {
RelocationKind::Absolute => ObjRelocKind::Absolute,
RelocationKind::Elf(kind) => match arch {
ObjArchitecture::PowerPc => match kind {
R_PPC_ADDR16_LO => ObjRelocKind::PpcAddr16Lo,
R_PPC_ADDR16_HI => ObjRelocKind::PpcAddr16Hi,
R_PPC_ADDR16_HA => ObjRelocKind::PpcAddr16Ha,
R_PPC_REL24 => ObjRelocKind::PpcRel24,
R_PPC_REL14 => ObjRelocKind::PpcRel14,
R_PPC_EMB_SDA21 => ObjRelocKind::PpcEmbSda21,
_ => {
return Err(anyhow::Error::msg(format!(
"Unhandled PPC relocation type: {kind}"
)))
}
},
ObjArchitecture::Mips => match kind {
R_MIPS_26 => ObjRelocKind::Mips26,
R_MIPS_HI16 => ObjRelocKind::MipsHi16,
R_MIPS_LO16 => ObjRelocKind::MipsLo16,
_ => {
return Err(anyhow::Error::msg(format!(
"Unhandled MIPS relocation type: {kind}"
)))
}
},
},
_ => {
return Err(anyhow::Error::msg(format!(
"Unhandled relocation type: {:?}",
reloc.kind()
)))
}
};
let target_section = match symbol.section() {
SymbolSection::Common => Some(".comm".to_string()),
SymbolSection::Section(idx) => {
obj_file.section_by_index(idx).and_then(|s| s.name().map(|s| s.to_string())).ok()
}
_ => None,
};
// println!("Reloc: {:?}, symbol: {:?}", reloc, symbol);
let target = match symbol.kind() {
SymbolKind::Text | SymbolKind::Data | SymbolKind::Unknown => {
to_obj_symbol(obj_file, &symbol, reloc.addend())
}
SymbolKind::Section => {
let addend = if reloc.has_implicit_addend() {
let addend = u32::from_be_bytes(
section.data[address as usize..address as usize + 4].try_into()?,
);
match kind {
ObjRelocKind::Absolute => addend,
ObjRelocKind::MipsHi16 | ObjRelocKind::MipsLo16 => addend & 0x0000FFFF,
ObjRelocKind::Mips26 => (addend & 0x03FFFFFF) * 4,
_ => todo!(),
}
} else {
let addend = reloc.addend();
if addend < 0 {
return Err(anyhow::Error::msg(format!(
"Negative addend in section reloc: {addend}"
)));
}
addend as u32
};
find_section_symbol(obj_file, &symbol, addend as u64)
}
_ => Err(anyhow::Error::msg(format!(
"Unhandled relocation symbol type {:?}",
symbol.kind()
))),
}?;
relocations.push(ObjReloc { kind, address, target, target_section });
}
Ok(relocations)
}
pub fn read(obj_path: &Path) -> Result<ObjInfo> {
let data = {
let file = fs::File::open(obj_path)?;
unsafe { memmap2::Mmap::map(&file) }?
};
let obj_file = File::parse(&*data)?;
let architecture = match obj_file.architecture() {
Architecture::PowerPc => ObjArchitecture::PowerPc,
Architecture::Mips => ObjArchitecture::Mips,
_ => {
return Err(anyhow::Error::msg(format!(
"Unsupported architecture: {:?}",
obj_file.architecture()
)))
}
};
let mut result = ObjInfo {
architecture,
path: obj_path.to_owned(),
sections: filter_sections(&obj_file)?,
common: common_symbols(&obj_file)?,
};
for section in &mut result.sections {
section.symbols = symbols_by_section(&obj_file, section)?;
section.relocations = relocations_by_section(architecture, &obj_file, section)?;
}
Ok(result)
}

View File

@@ -1,74 +0,0 @@
use anyhow::Result;
use rabbitizer::{config_set_register_fpr_abi_names, Abi, Instruction, SimpleOperandType};
use crate::obj::{ObjIns, ObjInsArg, ObjReloc};
pub fn process_code(
data: &[u8],
start_address: u64,
end_address: u64,
relocs: &[ObjReloc],
) -> Result<(Vec<u8>, Vec<ObjIns>)> {
config_set_register_fpr_abi_names(Abi::RABBITIZER_ABI_O32);
let ins_count = data.len() / 4;
let mut ops = Vec::<u8>::with_capacity(ins_count);
let mut insts = Vec::<ObjIns>::with_capacity(ins_count);
let mut cur_addr = start_address as u32;
for chunk in data.chunks_exact(4) {
let reloc = relocs.iter().find(|r| (r.address as u32 & !3) == cur_addr);
let code = u32::from_be_bytes(chunk.try_into()?);
let mut instruction = Instruction::new(code, cur_addr);
let op = instruction.instr_id() as u8;
ops.push(op);
let mnemonic = instruction.instr_id().get_opcode_name().unwrap_or_default().to_string();
let is_branch = instruction.is_branch();
let branch_offset = instruction.branch_offset();
let branch_dest =
if is_branch { Some((cur_addr as i32 + branch_offset) as u32) } else { None };
let args = instruction
.simple_operands()
.iter()
.map(|op| match op.kind {
SimpleOperandType::Imm | SimpleOperandType::Label => {
if is_branch {
ObjInsArg::BranchOffset(branch_offset)
} else if let Some(reloc) = reloc {
if matches!(&reloc.target_section, Some(s) if s == ".text")
&& reloc.target.address > start_address
&& reloc.target.address < end_address
{
// Inter-function reloc, convert to branch offset
ObjInsArg::BranchOffset(reloc.target.address as i32 - cur_addr as i32)
} else {
ObjInsArg::Reloc
}
} else {
ObjInsArg::MipsArg(op.disassembled.clone())
}
}
SimpleOperandType::ImmBase => {
if reloc.is_some() {
ObjInsArg::RelocWithBase
} else {
ObjInsArg::MipsArg(op.disassembled.clone())
}
}
_ => ObjInsArg::MipsArg(op.disassembled.clone()),
})
.collect();
insts.push(ObjIns {
address: cur_addr,
code,
op,
mnemonic,
args,
reloc: reloc.cloned(),
branch_dest,
});
cur_addr += 4;
}
Ok((ops, insts))
}

View File

@@ -1,165 +0,0 @@
pub mod elf;
pub mod mips;
pub mod ppc;
use std::path::PathBuf;
use flagset::{flags, FlagSet};
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
pub enum ObjSectionKind {
Code,
Data,
Bss,
}
flags! {
pub enum ObjSymbolFlags: u8 {
Global,
Local,
Weak,
Common,
}
}
#[derive(Debug, Copy, Clone, Default)]
pub struct ObjSymbolFlagSet(pub(crate) FlagSet<ObjSymbolFlags>);
#[derive(Debug, Clone)]
pub struct ObjSection {
pub name: String,
pub kind: ObjSectionKind,
pub address: u64,
pub size: u64,
pub data: Vec<u8>,
pub index: usize,
pub symbols: Vec<ObjSymbol>,
pub relocations: Vec<ObjReloc>,
// Diff
pub data_diff: Vec<ObjDataDiff>,
pub match_percent: f32,
}
#[derive(Debug, Clone)]
pub enum ObjInsArg {
PpcArg(ppc750cl::Argument),
MipsArg(String),
Reloc,
RelocWithBase,
BranchOffset(i32),
}
#[derive(Debug, Copy, Clone)]
pub struct ObjInsArgDiff {
/// Incrementing index for coloring
pub idx: usize,
}
#[derive(Debug, Clone)]
pub struct ObjInsBranchFrom {
/// Source instruction indices
pub ins_idx: Vec<usize>,
/// Incrementing index for coloring
pub branch_idx: usize,
}
#[derive(Debug, Clone)]
pub struct ObjInsBranchTo {
/// Target instruction index
pub ins_idx: usize,
/// Incrementing index for coloring
pub branch_idx: usize,
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
pub enum ObjInsDiffKind {
#[default]
None,
OpMismatch,
ArgMismatch,
Replace,
Delete,
Insert,
}
#[derive(Debug, Clone)]
pub struct ObjIns {
pub address: u32,
pub code: u32,
pub op: u8,
pub mnemonic: String,
pub args: Vec<ObjInsArg>,
pub reloc: Option<ObjReloc>,
pub branch_dest: Option<u32>,
}
#[derive(Debug, Clone, Default)]
pub struct ObjInsDiff {
pub ins: Option<ObjIns>,
/// Diff kind
pub kind: ObjInsDiffKind,
/// Branches from instruction
pub branch_from: Option<ObjInsBranchFrom>,
/// Branches to instruction
pub branch_to: Option<ObjInsBranchTo>,
/// Arg diffs
pub arg_diff: Vec<Option<ObjInsArgDiff>>,
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
pub enum ObjDataDiffKind {
#[default]
None,
Replace,
Delete,
Insert,
}
#[derive(Debug, Clone, Default)]
pub struct ObjDataDiff {
pub data: Vec<u8>,
pub kind: ObjDataDiffKind,
pub len: usize,
pub symbol: String,
}
#[derive(Debug, Clone)]
pub struct ObjSymbol {
pub name: String,
pub demangled_name: Option<String>,
pub address: u64,
pub section_address: u64,
pub size: u64,
pub size_known: bool,
pub flags: ObjSymbolFlagSet,
pub addend: i64,
// Diff
pub diff_symbol: Option<String>,
pub instructions: Vec<ObjInsDiff>,
pub match_percent: Option<f32>,
}
#[derive(Debug, Copy, Clone)]
pub enum ObjArchitecture {
PowerPc,
Mips,
}
#[derive(Debug, Clone)]
pub struct ObjInfo {
pub architecture: ObjArchitecture,
pub path: PathBuf,
pub sections: Vec<ObjSection>,
pub common: Vec<ObjSymbol>,
}
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
pub enum ObjRelocKind {
Absolute,
PpcAddr16Hi,
PpcAddr16Ha,
PpcAddr16Lo,
// PpcAddr32,
// PpcRel32,
// PpcAddr24,
PpcRel24,
// PpcAddr14,
PpcRel14,
PpcEmbSda21,
Mips26,
MipsHi16,
MipsLo16,
}
#[derive(Debug, Clone)]
pub struct ObjReloc {
pub kind: ObjRelocKind,
pub address: u64,
pub target: ObjSymbol,
pub target_section: Option<String>,
}

View File

@@ -1,88 +0,0 @@
use anyhow::Result;
use ppc750cl::{disasm_iter, Argument};
use crate::obj::{ObjIns, ObjInsArg, ObjReloc, ObjRelocKind};
// Relative relocation, can be Simm or BranchOffset
fn is_relative_arg(arg: &ObjInsArg) -> bool {
matches!(arg, ObjInsArg::PpcArg(Argument::Simm(_)) | ObjInsArg::BranchOffset(_))
}
// Relative or absolute relocation, can be Uimm, Simm or Offset
fn is_rel_abs_arg(arg: &ObjInsArg) -> bool {
matches!(arg, ObjInsArg::PpcArg(arg) if matches!(arg, Argument::Uimm(_) | Argument::Simm(_) | Argument::Offset(_)))
}
fn is_offset_arg(arg: &ObjInsArg) -> bool { matches!(arg, ObjInsArg::PpcArg(Argument::Offset(_))) }
pub fn process_code(
data: &[u8],
address: u64,
relocs: &[ObjReloc],
) -> Result<(Vec<u8>, Vec<ObjIns>)> {
let ins_count = data.len() / 4;
let mut ops = Vec::<u8>::with_capacity(ins_count);
let mut insts = Vec::<ObjIns>::with_capacity(ins_count);
for mut ins in disasm_iter(data, address as u32) {
let reloc = relocs.iter().find(|r| (r.address as u32 & !3) == ins.addr);
if let Some(reloc) = reloc {
// Zero out relocations
ins.code = match reloc.kind {
ObjRelocKind::PpcEmbSda21 => ins.code & !0x1FFFFF,
ObjRelocKind::PpcRel24 => ins.code & !0x3FFFFFC,
ObjRelocKind::PpcRel14 => ins.code & !0xFFFC,
ObjRelocKind::PpcAddr16Hi
| ObjRelocKind::PpcAddr16Ha
| ObjRelocKind::PpcAddr16Lo => ins.code & !0xFFFF,
_ => ins.code,
};
}
let simplified = ins.simplified();
let mut args: Vec<ObjInsArg> = simplified
.args
.iter()
.map(|a| match a {
Argument::BranchDest(dest) => ObjInsArg::BranchOffset(dest.0),
_ => ObjInsArg::PpcArg(a.clone()),
})
.collect();
if let Some(reloc) = reloc {
match reloc.kind {
ObjRelocKind::PpcEmbSda21 => {
args = vec![args[0].clone(), ObjInsArg::Reloc];
}
ObjRelocKind::PpcRel24 | ObjRelocKind::PpcRel14 => {
let arg = args
.iter_mut()
.rfind(|a| is_relative_arg(a))
.ok_or_else(|| anyhow::Error::msg("Failed to locate rel arg for reloc"))?;
*arg = ObjInsArg::Reloc;
}
ObjRelocKind::PpcAddr16Hi
| ObjRelocKind::PpcAddr16Ha
| ObjRelocKind::PpcAddr16Lo => {
let arg = args.iter_mut().rfind(|a| is_rel_abs_arg(a)).ok_or_else(|| {
anyhow::Error::msg("Failed to locate rel/abs arg for reloc")
})?;
*arg = if is_offset_arg(arg) {
ObjInsArg::RelocWithBase
} else {
ObjInsArg::Reloc
};
}
_ => {}
}
}
ops.push(simplified.ins.op as u8);
insts.push(ObjIns {
address: simplified.ins.addr,
code: simplified.ins.code,
mnemonic: format!("{}{}", simplified.mnemonic, simplified.suffix),
args,
reloc: reloc.cloned(),
op: 0,
branch_dest: None,
});
}
Ok((ops, insts))
}

View File

@@ -1,252 +0,0 @@
#[cfg(windows)]
use std::string::FromUtf16Error;
use std::sync::{Arc, RwLock};
#[cfg(windows)]
use anyhow::{Context, Result};
use const_format::formatcp;
use egui::{output::OpenUrl, Color32};
use self_update::cargo_crate_version;
use crate::{
app::{AppConfig, DiffKind, ViewState},
jobs::{bindiff::queue_bindiff, objdiff::queue_build, update::queue_update},
update::RELEASE_URL,
};
#[cfg(windows)]
fn process_utf16(bytes: &[u8]) -> Result<String, FromUtf16Error> {
let u16_bytes: Vec<u16> = bytes
.chunks_exact(2)
.filter_map(|c| Some(u16::from_ne_bytes(c.try_into().ok()?)))
.collect();
String::from_utf16(&u16_bytes)
}
#[cfg(windows)]
fn wsl_cmd(args: &[&str]) -> Result<String> {
use std::{os::windows::process::CommandExt, process::Command};
let output = Command::new("wsl")
.args(args)
.creation_flags(winapi::um::winbase::CREATE_NO_WINDOW)
.output()
.context("Failed to execute wsl")?;
process_utf16(&output.stdout).context("Failed to process stdout")
}
#[cfg(windows)]
fn fetch_wsl2_distros() -> Vec<String> {
wsl_cmd(&["-l", "-q"])
.map(|stdout| {
stdout
.split('\n')
.filter(|s| !s.trim().is_empty())
.map(|s| s.trim().to_string())
.collect()
})
.unwrap_or_default()
}
pub fn config_ui(ui: &mut egui::Ui, config: &Arc<RwLock<AppConfig>>, view_state: &mut ViewState) {
let mut config_guard = config.write().unwrap();
let AppConfig {
custom_make,
available_wsl_distros,
selected_wsl_distro,
project_dir,
target_obj_dir,
base_obj_dir,
obj_path,
build_target,
left_obj,
right_obj,
project_dir_change,
queue_update_check,
auto_update_check,
} = &mut *config_guard;
ui.heading("Updates");
ui.checkbox(auto_update_check, "Check for updates on startup");
if ui.button("Check now").clicked() {
*queue_update_check = true;
}
ui.label(format!("Current version: {}", cargo_crate_version!())).on_hover_ui_at_pointer(|ui| {
ui.label(formatcp!("Git branch: {}", env!("VERGEN_GIT_BRANCH")));
ui.label(formatcp!("Git commit: {}", env!("VERGEN_GIT_SHA")));
ui.label(formatcp!("Build target: {}", env!("VERGEN_CARGO_TARGET_TRIPLE")));
ui.label(formatcp!("Build type: {}", env!("VERGEN_CARGO_PROFILE")));
});
if let Some(state) = &view_state.check_update {
ui.label(format!("Latest version: {}", state.latest_release.version));
if state.update_available {
ui.colored_label(Color32::LIGHT_GREEN, "Update available");
ui.horizontal(|ui| {
if state.found_binary && ui
.button("Automatic")
.on_hover_text_at_pointer(
"Automatically download and replace the current build",
)
.clicked() {
view_state.jobs.push(queue_update());
}
if ui
.button("Manual")
.on_hover_text_at_pointer("Open a link to the latest release on GitHub")
.clicked()
{
ui.output().open_url =
Some(OpenUrl { url: RELEASE_URL.to_string(), new_tab: true });
}
});
}
}
ui.separator();
ui.heading("Build config");
#[cfg(windows)]
{
if available_wsl_distros.is_none() {
*available_wsl_distros = Some(fetch_wsl2_distros());
}
egui::ComboBox::from_label("Run in WSL2")
.selected_text(selected_wsl_distro.as_ref().unwrap_or(&"None".to_string()))
.show_ui(ui, |ui| {
ui.selectable_value(selected_wsl_distro, None, "None");
for distro in available_wsl_distros.as_ref().unwrap() {
ui.selectable_value(selected_wsl_distro, Some(distro.clone()), distro);
}
});
}
#[cfg(not(windows))]
{
let _ = available_wsl_distros;
let _ = selected_wsl_distro;
}
ui.label("Custom make program:");
let mut custom_make_str = custom_make.clone().unwrap_or_default();
if ui.text_edit_singleline(&mut custom_make_str).changed() {
if custom_make_str.is_empty() {
*custom_make = None;
} else {
*custom_make = Some(custom_make_str);
}
}
ui.separator();
ui.heading("Project config");
if view_state.diff_kind == DiffKind::SplitObj {
if ui.button("Select project dir").clicked() {
if let Some(path) = rfd::FileDialog::new().pick_folder() {
*project_dir = Some(path);
*project_dir_change = true;
*target_obj_dir = None;
*base_obj_dir = None;
*obj_path = None;
}
}
if let Some(dir) = project_dir {
ui.label(dir.to_string_lossy());
}
ui.separator();
if let Some(project_dir) = project_dir {
if ui.button("Select target build dir").clicked() {
if let Some(path) = rfd::FileDialog::new().set_directory(&project_dir).pick_folder()
{
*target_obj_dir = Some(path);
*obj_path = None;
}
}
if let Some(dir) = target_obj_dir {
ui.label(dir.to_string_lossy());
}
ui.checkbox(build_target, "Build target");
ui.separator();
if ui.button("Select base build dir").clicked() {
if let Some(path) = rfd::FileDialog::new().set_directory(&project_dir).pick_folder()
{
*base_obj_dir = Some(path);
*obj_path = None;
}
}
if let Some(dir) = base_obj_dir {
ui.label(dir.to_string_lossy());
}
ui.separator();
}
if let Some(base_dir) = base_obj_dir {
if ui.button("Select obj").clicked() {
if let Some(path) = rfd::FileDialog::new()
.set_directory(&base_dir)
.add_filter("Object file", &["o", "elf"])
.pick_file()
{
let mut new_build_obj: Option<String> = None;
if let Ok(obj_path) = path.strip_prefix(&base_dir) {
new_build_obj = Some(obj_path.display().to_string());
} else if let Some(build_asm_dir) = target_obj_dir {
if let Ok(obj_path) = path.strip_prefix(&build_asm_dir) {
new_build_obj = Some(obj_path.display().to_string());
}
}
if let Some(new_build_obj) = new_build_obj {
*obj_path = Some(new_build_obj);
view_state
.jobs
.push(queue_build(config.clone(), view_state.diff_config.clone()));
}
}
}
if let Some(obj) = obj_path {
ui.label(&*obj);
if ui.button("Build").clicked() {
view_state
.jobs
.push(queue_build(config.clone(), view_state.diff_config.clone()));
}
}
ui.separator();
}
} else if view_state.diff_kind == DiffKind::WholeBinary {
if ui.button("Select left obj").clicked() {
if let Some(path) =
rfd::FileDialog::new().add_filter("Object file", &["o", "elf"]).pick_file()
{
*left_obj = Some(path);
}
}
if let Some(obj) = left_obj {
ui.label(obj.to_string_lossy());
}
if ui.button("Select right obj").clicked() {
if let Some(path) =
rfd::FileDialog::new().add_filter("Object file", &["o", "elf"]).pick_file()
{
*right_obj = Some(path);
}
}
if let Some(obj) = right_obj {
ui.label(obj.to_string_lossy());
}
if let (Some(_), Some(_)) = (left_obj, right_obj) {
if ui.button("Build").clicked() {
view_state.jobs.push(queue_bindiff(config.clone()));
}
}
}
ui.checkbox(&mut view_state.reverse_fn_order, "Reverse function order (deferred)");
ui.separator();
}

Some files were not shown because too many files have changed in this diff Show More