Compare commits

...

34 Commits

Author SHA1 Message Date
Luke Street 7834185223 Version 3.5.1 2025-12-23 12:33:12 -07:00
LagoLunatic 216dd70d5b Fix mouse hovers overriding typing in the symbol search field (#308) 2025-12-23 12:32:07 -07:00
LagoLunatic 976e629f08 Fix non-strings also being escaped when copied (#307)
* Fix non-strings also being escaped when copied

* Move copy escape logic into objdiff-core
2025-12-23 12:31:36 -07:00
Luke Street 67b28b7da1 Version 3.5.0 2025-12-21 10:02:59 -07:00
LagoLunatic c0b9bfae21 Fix special ASCII characters not being escaped when copied to clipboard (#306) 2025-12-21 09:58:45 -07:00
LagoLunatic f3f4b0482a Combine .init sections separately from .text sections (#304) 2025-12-21 09:58:13 -07:00
LagoLunatic 278683f103 Improve automatic symbol pairing for functions with compiler-generated names (#303)
* Fix `__arraydtor$1234`s sometimes being swapped

The list of symbols is not necessarily sorted by address, so if there are multiple `__arraydtor$1234` functions it's possible that they're in reverse order on the right, which would cause them to be paired up incorrectly.

To fix this we sort by section index and symbol address after filtering down the list of symbols to ones that have matching names.

* Support pairing up symbols for anonymous classes

* Disallow exact-name pairups for compiler-generated symbols
2025-12-21 09:57:17 -07:00
LagoLunatic 6ad60de32a Allow clicking on branch arrows to scroll target into view (#302) 2025-12-21 09:55:55 -07:00
Luke Street 90c93164ab Update deny.toml 2025-12-21 09:36:27 -07:00
Luke Street c02eb31dbb Version 3.4.5 2025-12-03 18:03:11 -07:00
Darxoon 86d92866aa Fix "Unsupported ARM implicit relocation 29" on armcc binaries (#296) 2025-12-03 17:59:24 -07:00
Luke Street 51c3af2bbe Version 3.4.4 2025-11-26 19:33:00 -07:00
LagoLunatic d0b8b449d9 Ignore hidden symbols when diffing data sections (#291) 2025-11-26 18:49:36 -07:00
LagoLunatic 32f5f202f7 Update outdated extab test snapshot (#292) 2025-11-26 17:56:32 -05:00
LagoLunatic 481dbc185a Support more string encodings and allow copying unescaped strings (#288)
* Copy strings without escape sequences in them

* Add support for more encodings from encoding_rs

Also use encoding_rs instead of CStr for UTF-8.
2025-11-22 19:36:41 -07:00
Luke Street 26a4cc79cf Version 3.4.3 2025-11-22 12:47:03 -07:00
Luke Street 5c96c2ee51 Update cwextab 2025-11-22 12:45:58 -07:00
Luke Street d162fe847e Remove --mapping, --selecting-{left,right} from CLI 2025-11-21 23:55:26 -07:00
Luke Street 2a24eb5aec Version 3.4.2 2025-11-21 22:15:20 -07:00
Aetias f7c291bd55 arm: Fix .word reading 4 bytes when less than 4 remain (#285) 2025-11-20 15:38:55 -07:00
Luke Street 03a578c1bb Update test snapshots for new cwextab 2025-11-18 22:29:42 -07:00
Luke Street d09ef8e207 Version v3.4.1 2025-11-18 22:22:43 -07:00
Luke Street 63552a58ae Move copy button to first header row 2025-11-18 22:22:02 -07:00
Luke Street 827f4a42bd Upgrade all dependencies (incl. egui) 2025-11-18 22:21:46 -07:00
Franco M b2dcecc5d8 Copy button (#271)
* Update diff.rs

* Fixes

* More fixes

* More fixes...

* Cargo fmt

* fmt

* Trim extra

* cargo +nightly fmt

* Update 'Copy ASM' button text to include emoji
2025-11-18 22:00:20 -07:00
Dávid Balatoni 67b237eab6 Adjust symbol name matching logic for GCC (#278)
* Adjust symbol name matching logic for GCC

* Turn $ and . into a list

* Fix borrow issue
2025-11-18 21:55:55 -07:00
Aetias 66da80ff69 arm: Fix .word not on 4-byte boundary (#282) 2025-11-18 21:55:40 -07:00
Luke Street 0a85f540f2 Version v3.4.0 2025-11-12 12:22:01 -07:00
Anghelo Carvajal d79b5b1233 Update gnuv2_demangle to 0.4 (#280) 2025-11-09 11:53:02 -07:00
Anghelo Carvajal 8f5519cb6a Update gnuv2_demangle to version 0.3 (#275) 2025-10-12 21:19:07 -06:00
Aetias 19ec08be9a Update unarm to 2.0 (#274)
* Migrate to unarm 2.0

* Update unarm to proper 2.0 release

* Deduplicate formatters for opaque instruction parts in ARM

* Add option to enable VFPv2 for ARM

* Update unarm to 2.0.1

* Fix read order for big-endian Thumb code

* Skip leading space in ARM instruction params

* Update ARM tests

* arm.rs: Use `alloc::borrow::Cow`
2025-10-12 21:13:29 -06:00
Luke Street 2ad0898efa Version v3.3.1 2025-10-08 21:47:36 -06:00
Ethan Lafrenais 03f2bcb8b1 R_MIPS_GPREL32 Support (#270)
* R_MIPS_GPREL32 Support

* cargo fmt

* Add name for R_MIPS_GPREL32 relocation
2025-10-08 20:34:35 -06:00
BR- 781071761a show symbol size in symbol list (#268)
* show symbol size in symbol list

* configs for show symbol size

* move symbol size config to config-schema
2025-10-08 20:32:07 -06:00
42 changed files with 1955 additions and 1780 deletions
Generated
+752 -728
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -14,7 +14,7 @@ default-members = [
resolver = "3" resolver = "3"
[workspace.package] [workspace.package]
version = "3.3.0" version = "3.5.1"
authors = ["Luke Street <luke@street.dev>"] authors = ["Luke Street <luke@street.dev>"]
edition = "2024" edition = "2024"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
+2
View File
@@ -75,6 +75,8 @@ ignore = [
#{ crate = "a-crate-that-is-yanked@0.1.1", reason = "you can specify why you are ignoring the yanked crate" }, #{ crate = "a-crate-that-is-yanked@0.1.1", reason = "you can specify why you are ignoring the yanked crate" },
{ id = "RUSTSEC-2024-0436", reason = "Unmaintained paste crate is an indirect dependency" }, { id = "RUSTSEC-2024-0436", reason = "Unmaintained paste crate is an indirect dependency" },
{ id = "RUSTSEC-2025-0052", reason = "Unmaintained async-std crate is an indirect dependency" }, { id = "RUSTSEC-2025-0052", reason = "Unmaintained async-std crate is an indirect dependency" },
{ id = "RUSTSEC-2025-0119", reason = "Unmaintained number_prefix crate is an indirect dependency" },
{ id = "RUSTSEC-2025-0134", reason = "Unmaintained rustls-pemfile crate is an indirect dependency" },
] ]
# If this is true, then cargo deny will use the git executable to fetch advisory database. # 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. # If this is false, then it uses a built-in git library.
+3 -3
View File
@@ -16,19 +16,19 @@ publish = false
anyhow = "1.0" anyhow = "1.0"
argp = "0.4" argp = "0.4"
crossterm = "0.29" crossterm = "0.29"
enable-ansi-support = "0.2" enable-ansi-support = "0.3"
memmap2 = "0.9" memmap2 = "0.9"
objdiff-core = { path = "../objdiff-core", features = ["all"] } objdiff-core = { path = "../objdiff-core", features = ["all"] }
prost = "0.14" prost = "0.14"
ratatui = "0.29" ratatui = "0.29"
rayon = "1.10" rayon = "1.11"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
supports-color = "3.0" supports-color = "3.0"
time = { version = "0.3", features = ["formatting", "local-offset"] } time = { version = "0.3", features = ["formatting", "local-offset"] }
tracing = "0.1" tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] }
typed-path = "0.11" typed-path = "0.12"
[target.'cfg(target_env = "musl")'.dependencies] [target.'cfg(target_env = "musl")'.dependencies]
mimalloc = "0.1" mimalloc = "0.1"
+1 -20
View File
@@ -66,15 +66,6 @@ pub struct Args {
#[argp(option, short = 'c')] #[argp(option, short = 'c')]
/// Configuration property (key=value) /// Configuration property (key=value)
config: Vec<String>, config: Vec<String>,
#[argp(option, short = 'm')]
/// Symbol mapping (target=base)
mapping: Vec<String>,
#[argp(option)]
/// Left symbol name for selection
selecting_left: Option<String>,
#[argp(option)]
/// Right symbol name for selection
selecting_right: Option<String>,
} }
pub fn run(args: Args) -> Result<()> { pub fn run(args: Args) -> Result<()> {
@@ -183,17 +174,7 @@ fn build_config_from_args(
apply_project_options(&mut diff_config, options)?; apply_project_options(&mut diff_config, options)?;
} }
apply_config_args(&mut diff_config, &args.config)?; apply_config_args(&mut diff_config, &args.config)?;
let mut mapping_config = MappingConfig { Ok((diff_config, MappingConfig::default()))
mappings: Default::default(),
selecting_left: args.selecting_left.clone(),
selecting_right: args.selecting_right.clone(),
};
for mapping in &args.mapping {
let (target, base) =
mapping.split_once('=').context("--mapping expects \"target=base\"")?;
mapping_config.mappings.insert(target.to_string(), base.to_string());
}
Ok((diff_config, mapping_config))
} }
pub struct AppState { pub struct AppState {
+1 -2
View File
@@ -378,7 +378,6 @@ impl UiView for FunctionDiffUi {
} }
// Reload // Reload
KeyCode::Char('r') => { KeyCode::Char('r') => {
result.redraw = true;
return EventControlFlow::Reload; return EventControlFlow::Reload;
} }
// Scroll right // Scroll right
@@ -400,7 +399,6 @@ impl UiView for FunctionDiffUi {
FunctionRelocDiffs::DataValue => FunctionRelocDiffs::All, FunctionRelocDiffs::DataValue => FunctionRelocDiffs::All,
FunctionRelocDiffs::All => FunctionRelocDiffs::None, FunctionRelocDiffs::All => FunctionRelocDiffs::None,
}; };
result.redraw = true;
return EventControlFlow::Reload; return EventControlFlow::Reload;
} }
// Toggle three-way diff // Toggle three-way diff
@@ -541,6 +539,7 @@ impl FunctionDiffUi {
DiffText::Opcode(mnemonic, _op) => format!("{mnemonic} "), DiffText::Opcode(mnemonic, _op) => format!("{mnemonic} "),
DiffText::Argument(arg) => arg.to_string(), DiffText::Argument(arg) => arg.to_string(),
DiffText::BranchDest(addr) => format!("{addr:x}"), DiffText::BranchDest(addr) => format!("{addr:x}"),
DiffText::BranchArrow(_) => " ~> ".to_string(),
DiffText::Symbol(sym) => { DiffText::Symbol(sym) => {
sym.demangled_name.as_ref().unwrap_or(&sym.name).clone() sym.demangled_name.as_ref().unwrap_or(&sym.name).clone()
} }
+9 -9
View File
@@ -135,10 +135,10 @@ num-traits = { version = "0.2", default-features = false, optional = true }
object = { version = "0.37", default-features = false, features = ["read_core", "elf", "coff"] } object = { version = "0.37", default-features = false, features = ["read_core", "elf", "coff"] }
pbjson = { version = "0.8", default-features = false, optional = true } pbjson = { version = "0.8", default-features = false, optional = true }
prost = { version = "0.14", default-features = false, features = ["derive"], optional = true } prost = { version = "0.14", default-features = false, features = ["derive"], optional = true }
regex = { version = "1.11", default-features = false, features = [], optional = true } regex = { version = "1.12", default-features = false, features = [], optional = true }
serde = { version = "1.0", default-features = false, features = ["derive"], optional = true } serde = { version = "1.0", default-features = false, features = ["derive"], optional = true }
similar = { git = "https://github.com/encounter/similar.git", branch = "no_std", default-features = false, features = ["hashbrown"], optional = true } similar = { git = "https://github.com/encounter/similar.git", branch = "no_std", default-features = false, features = ["hashbrown"], optional = true }
typed-path = { version = "0.11", default-features = false, optional = true } typed-path = { version = "0.12", default-features = false, optional = true }
# config # config
globset = { version = "0.4", default-features = false, optional = true } globset = { version = "0.4", default-features = false, optional = true }
@@ -155,13 +155,13 @@ powerpc = { version = "0.4", optional = true }
rlwinmdec = { version = "1.1", optional = true } rlwinmdec = { version = "1.1", optional = true }
# mips # mips
rabbitizer = { version = "2.0.0-alpha.4", default-features = false, features = ["all_extensions"], optional = true } rabbitizer = { version = "2.0.0-alpha.7", default-features = false, features = ["all_extensions"], optional = true }
# x86 # x86
iced-x86 = { version = "1.21", default-features = false, features = ["decoder", "intel", "gas", "masm", "nasm", "exhaustive_enums", "no_std"], optional = true } iced-x86 = { version = "1.21", default-features = false, features = ["decoder", "intel", "gas", "masm", "nasm", "exhaustive_enums", "no_std"], optional = true }
# arm # arm
unarm = { version = "1.9", optional = true } unarm = { version = "2.1", optional = true }
arm-attr = { version = "0.2", optional = true } arm-attr = { version = "0.2", optional = true }
# arm64 # arm64
@@ -169,17 +169,17 @@ yaxpeax-arch = { version = "0.3", default-features = false, optional = true }
yaxpeax-arm = { version = "0.3", default-features = false, optional = true } yaxpeax-arm = { version = "0.3", default-features = false, optional = true }
# build # build
notify = { version = "8.1.0", optional = true } notify = { version = "8.2.0", optional = true }
notify-debouncer-full = { version = "0.5.0", optional = true } notify-debouncer-full = { version = "0.6.0", optional = true }
shell-escape = { version = "0.1", optional = true } shell-escape = { version = "0.1", optional = true }
tempfile = { version = "3.20", optional = true } tempfile = { version = "3.23", optional = true }
time = { version = "0.3", optional = true } time = { version = "0.3", optional = true }
encoding_rs = { version = "0.8.35", optional = true } encoding_rs = { version = "0.8.35", optional = true }
# demangler # demangler
cpp_demangle = { version = "0.4", optional = true, default-features = false, features = ["alloc"] } cpp_demangle = { version = "0.5", optional = true, default-features = false, features = ["alloc"] }
cwdemangle = { version = "1.0", optional = true } cwdemangle = { version = "1.0", optional = true }
gnuv2_demangle = { version = "0.1.0", optional = true } gnuv2_demangle = { version = "0.4", optional = true }
msvc-demangler = { version = "0.11", optional = true } msvc-demangler = { version = "0.11", optional = true }
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
+46
View File
@@ -82,6 +82,27 @@
"name": "Space between args", "name": "Space between args",
"description": "Adds a space between arguments in the diff output." "description": "Adds a space between arguments in the diff output."
}, },
{
"id": "showSymbolSizes",
"type": "choice",
"default": "off",
"name": "Show symbol sizes",
"description": "Shows symbol sizes in the symbol view.",
"items": [
{
"value": "off",
"name": "Off"
},
{
"value": "hex",
"name": "Hex"
},
{
"value": "decimal",
"name": "Decimal"
}
]
},
{ {
"id": "combineDataSections", "id": "combineDataSections",
"type": "boolean", "type": "boolean",
@@ -107,20 +128,43 @@
"value": "auto", "value": "auto",
"name": "Auto" "name": "Auto"
}, },
{
"value": "v4",
"name": "ARMv4"
},
{ {
"value": "v4t", "value": "v4t",
"name": "ARMv4T (GBA)" "name": "ARMv4T (GBA)"
}, },
{
"value": "v5t",
"name": "ARMv5T"
},
{ {
"value": "v5te", "value": "v5te",
"name": "ARMv5TE (DS)" "name": "ARMv5TE (DS)"
}, },
{
"value": "v5tej",
"name": "ARMv5TEJ"
},
{
"value": "v6",
"name": "ARMv6"
},
{ {
"value": "v6k", "value": "v6k",
"name": "ARMv6K (3DS)" "name": "ARMv6K (3DS)"
} }
] ]
}, },
{
"id": "arm.vfpV2",
"type": "boolean",
"default": true,
"name": "VFPv2 instructions",
"description": "Adds floating-point instructions from VFPv2."
},
{ {
"id": "arm.unifiedSyntax", "id": "arm.unifiedSyntax",
"type": "boolean", "type": "boolean",
@@ -296,6 +340,7 @@
"properties": [ "properties": [
"functionRelocDiffs", "functionRelocDiffs",
"demangler", "demangler",
"showSymbolSizes",
"spaceBetweenArgs", "spaceBetweenArgs",
"combineDataSections", "combineDataSections",
"combineTextSections" "combineTextSections"
@@ -306,6 +351,7 @@
"name": "ARM", "name": "ARM",
"properties": [ "properties": [
"arm.archVersion", "arm.archVersion",
"arm.vfpV2",
"arm.unifiedSyntax", "arm.unifiedSyntax",
"arm.avRegisters", "arm.avRegisters",
"arm.r9Usage", "arm.r9Usage",
File diff suppressed because it is too large Load Diff
+2
View File
@@ -293,6 +293,7 @@ impl Arch for ArchMips {
} }
elf::R_MIPS_PC16 => 0, // PC-relative relocation elf::R_MIPS_PC16 => 0, // PC-relative relocation
R_MIPS15_S3 => ((code & 0x001FFFC0) >> 3) as i64, R_MIPS15_S3 => ((code & 0x001FFFC0) >> 3) as i64,
elf::R_MIPS_GPREL32 => (code as i32 as i64) + self.ri_gp_value as i64,
flags => bail!("Unsupported MIPS implicit relocation {flags:?}"), flags => bail!("Unsupported MIPS implicit relocation {flags:?}"),
}; };
Ok(Some(RelocationOverride { target: RelocationOverrideTarget::Keep, addend })) Ok(Some(RelocationOverride { target: RelocationOverrideTarget::Keep, addend }))
@@ -318,6 +319,7 @@ impl Arch for ArchMips {
elf::R_MIPS_GOT16 => Some("R_MIPS_GOT16"), elf::R_MIPS_GOT16 => Some("R_MIPS_GOT16"),
elf::R_MIPS_PC16 => Some("R_MIPS_PC16"), elf::R_MIPS_PC16 => Some("R_MIPS_PC16"),
elf::R_MIPS_CALL16 => Some("R_MIPS_CALL16"), elf::R_MIPS_CALL16 => Some("R_MIPS_CALL16"),
elf::R_MIPS_GPREL32 => Some("R_MIPS_GPREL32"),
R_MIPS15_S3 => Some("R_MIPS15_S3"), R_MIPS15_S3 => Some("R_MIPS15_S3"),
_ => None, _ => None,
}, },
+57 -23
View File
@@ -7,12 +7,10 @@ use alloc::{
}; };
use core::{ use core::{
any::Any, any::Any,
ffi::CStr,
fmt::{self, Debug}, fmt::{self, Debug},
}; };
use anyhow::{Result, bail}; use anyhow::{Result, bail};
use encoding_rs::SHIFT_JIS;
use object::Endian as _; use object::Endian as _;
use crate::{ use crate::{
@@ -44,6 +42,16 @@ pub mod x86;
pub const OPCODE_INVALID: u16 = u16::MAX; pub const OPCODE_INVALID: u16 = u16::MAX;
pub const OPCODE_DATA: u16 = u16::MAX - 1; pub const OPCODE_DATA: u16 = u16::MAX - 1;
const SUPPORTED_ENCODINGS: [(&encoding_rs::Encoding, &str); 7] = [
(encoding_rs::UTF_8, "UTF-8"),
(encoding_rs::SHIFT_JIS, "Shift JIS"),
(encoding_rs::UTF_16BE, "UTF-16BE"),
(encoding_rs::UTF_16LE, "UTF-16LE"),
(encoding_rs::WINDOWS_1252, "Windows-1252"),
(encoding_rs::EUC_JP, "EUC-JP"),
(encoding_rs::BIG5, "Big5"),
];
/// Represents the type of data associated with an instruction /// Represents the type of data associated with an instruction
#[derive(PartialEq)] #[derive(PartialEq)]
pub enum DataType { pub enum DataType {
@@ -75,9 +83,9 @@ impl fmt::Display for DataType {
impl DataType { impl DataType {
pub fn display_labels(&self, endian: object::Endianness, bytes: &[u8]) -> Vec<String> { pub fn display_labels(&self, endian: object::Endianness, bytes: &[u8]) -> Vec<String> {
let mut strs = Vec::new(); let mut strs = Vec::new();
for (literal, label_override) in self.display_literals(endian, bytes) { for (literal, label_override, _) in self.display_literals(endian, bytes) {
let label = label_override.unwrap_or_else(|| self.to_string()); let label = label_override.unwrap_or_else(|| self.to_string());
strs.push(format!("{label}: {literal}")) strs.push(format!("{label}: {literal:?}"))
} }
strs strs
} }
@@ -86,7 +94,7 @@ impl DataType {
&self, &self,
endian: object::Endianness, endian: object::Endianness,
bytes: &[u8], bytes: &[u8],
) -> Vec<(String, Option<String>)> { ) -> Vec<(String, Option<String>, Option<String>)> {
let mut strs = Vec::new(); let mut strs = Vec::new();
if self.required_len().is_some_and(|l| bytes.len() < l) { if self.required_len().is_some_and(|l| bytes.len() < l) {
log::warn!( log::warn!(
@@ -110,34 +118,34 @@ impl DataType {
match self { match self {
DataType::Int8 => { DataType::Int8 => {
let i = i8::from_ne_bytes(bytes.try_into().unwrap()); let i = i8::from_ne_bytes(bytes.try_into().unwrap());
strs.push((format!("{i:#x}"), None)); strs.push((format!("{i:#x}"), None, None));
if i < 0 { if i < 0 {
strs.push((format!("{:#x}", ReallySigned(i)), None)); strs.push((format!("{:#x}", ReallySigned(i)), None, None));
} }
} }
DataType::Int16 => { DataType::Int16 => {
let i = endian.read_i16_bytes(bytes.try_into().unwrap()); let i = endian.read_i16_bytes(bytes.try_into().unwrap());
strs.push((format!("{i:#x}"), None)); strs.push((format!("{i:#x}"), None, None));
if i < 0 { if i < 0 {
strs.push((format!("{:#x}", ReallySigned(i)), None)); strs.push((format!("{:#x}", ReallySigned(i)), None, None));
} }
} }
DataType::Int32 => { DataType::Int32 => {
let i = endian.read_i32_bytes(bytes.try_into().unwrap()); let i = endian.read_i32_bytes(bytes.try_into().unwrap());
strs.push((format!("{i:#x}"), None)); strs.push((format!("{i:#x}"), None, None));
if i < 0 { if i < 0 {
strs.push((format!("{:#x}", ReallySigned(i)), None)); strs.push((format!("{:#x}", ReallySigned(i)), None, None));
} }
} }
DataType::Int64 => { DataType::Int64 => {
let i = endian.read_i64_bytes(bytes.try_into().unwrap()); let i = endian.read_i64_bytes(bytes.try_into().unwrap());
strs.push((format!("{i:#x}"), None)); strs.push((format!("{i:#x}"), None, None));
if i < 0 { if i < 0 {
strs.push((format!("{:#x}", ReallySigned(i)), None)); strs.push((format!("{:#x}", ReallySigned(i)), None, None));
} }
} }
DataType::Float => { DataType::Float => {
@@ -148,6 +156,7 @@ impl DataType {
object::Endianness::Big => f32::from_be_bytes(bytes), object::Endianness::Big => f32::from_be_bytes(bytes),
}), }),
None, None,
None,
)); ));
} }
DataType::Double => { DataType::Double => {
@@ -158,22 +167,29 @@ impl DataType {
object::Endianness::Big => f64::from_be_bytes(bytes), object::Endianness::Big => f64::from_be_bytes(bytes),
}), }),
None, None,
None,
)); ));
} }
DataType::Bytes => { DataType::Bytes => {
strs.push((format!("{bytes:#?}"), None)); strs.push((format!("{bytes:#?}"), None, None));
} }
DataType::String => { DataType::String => {
if let Ok(cstr) = CStr::from_bytes_until_nul(bytes) {
strs.push((format!("{cstr:?}"), None));
}
if let Some(nul_idx) = bytes.iter().position(|&c| c == b'\0') { if let Some(nul_idx) = bytes.iter().position(|&c| c == b'\0') {
let (cow, _, had_errors) = SHIFT_JIS.decode(&bytes[..nul_idx]); let str_bytes = &bytes[..nul_idx];
if !had_errors { // Special case to display (ASCII) as the label for ASCII-only strings.
let str = format!("{cow:?}"); let (cow, _, had_errors) = encoding_rs::UTF_8.decode(str_bytes);
// Only add the Shift JIS string if it's different from the ASCII string. if !had_errors && cow.is_ascii() {
if !strs.iter().any(|x| x.0 == str) { let string = format!("{cow}");
strs.push((str, Some("Shift JIS".into()))); let copy_string = escape_special_ascii_characters(string.clone());
strs.push((string, Some("ASCII".into()), Some(copy_string)));
}
for (encoding, encoding_name) in SUPPORTED_ENCODINGS {
let (cow, _, had_errors) = encoding.decode(str_bytes);
// Avoid showing ASCII-only strings more than once if the encoding is ASCII-compatible.
if !had_errors && (!encoding.is_ascii_compatible() || !cow.is_ascii()) {
let string = format!("{cow}");
let copy_string = escape_special_ascii_characters(string.clone());
strs.push((string, Some(encoding_name.into()), Some(copy_string)));
} }
} }
} }
@@ -489,3 +505,21 @@ pub struct RelocationOverride {
pub target: RelocationOverrideTarget, pub target: RelocationOverrideTarget,
pub addend: i64, pub addend: i64,
} }
/// Escape ASCII characters such as \n or \t, but not Unicode characters such as \u{3000}.
/// Suitable for copying to clipboard.
fn escape_special_ascii_characters(value: String) -> String {
let mut escaped = String::new();
escaped.push('"');
for c in value.chars() {
if c.is_ascii() {
for e in c.escape_default() {
escaped.push(e);
}
} else {
escaped.push(c);
}
}
escaped.push('"');
escaped
}
+6 -2
View File
@@ -440,9 +440,13 @@ impl Arch for ArchPpc {
let simplified = ins.simplified().to_string(); let simplified = ins.simplified().to_string();
let show_orig = orig != simplified; let show_orig = orig != simplified;
let mut out = Vec::with_capacity(2); let mut out = Vec::with_capacity(2);
out.push(ContextItem::Copy { value: simplified, label: None }); out.push(ContextItem::Copy { value: simplified, label: None, copy_string: None });
if show_orig { if show_orig {
out.push(ContextItem::Copy { value: orig, label: Some("original".to_string()) }); out.push(ContextItem::Copy {
value: orig,
label: Some("original".to_string()),
copy_string: None,
});
} }
out out
} }
+20 -5
View File
@@ -36,13 +36,27 @@ pub fn diff_bss_symbol(
} }
pub fn symbol_name_matches(left_name: &str, right_name: &str) -> bool { pub fn symbol_name_matches(left_name: &str, right_name: &str) -> bool {
// Match Metrowerks symbol$1234 against symbol$2345 if let Some((left_prefix, left_suffix)) = left_name.split_once("@class$")
if let Some((prefix, suffix)) = left_name.split_once('$') { && let Some((right_prefix, right_suffix)) = right_name.split_once("@class$")
if !suffix.chars().all(char::is_numeric) { {
return false; // Match Metrowerks anonymous class symbol names, ignoring the unique ID.
// e.g. __dt__Q29dCamera_c23@class$3665d_camera_cppFv
if left_prefix == right_prefix
&& let Some(left_idx) = left_suffix.chars().position(|c| !c.is_numeric())
&& let Some(right_idx) = right_suffix.chars().position(|c| !c.is_numeric())
{
// e.g. d_camera_cppFv (after the unique ID)
left_suffix[left_idx..] == right_suffix[right_idx..]
} else {
false
} }
} else if let Some((prefix, suffix)) = left_name.split_once(['$', '.'])
&& suffix.chars().all(char::is_numeric)
{
// Match Metrowerks symbol$1234 against symbol$2345
// and GCC symbol.1234 against symbol.2345
right_name right_name
.split_once('$') .split_once(['$', '.'])
.is_some_and(|(p, s)| p == prefix && s.chars().all(char::is_numeric)) .is_some_and(|(p, s)| p == prefix && s.chars().all(char::is_numeric))
} else { } else {
left_name == right_name left_name == right_name
@@ -587,6 +601,7 @@ fn symbols_matching_section(
s.section == Some(section_idx) s.section == Some(section_idx)
&& s.kind != SymbolKind::Section && s.kind != SymbolKind::Section
&& s.size > 0 && s.size > 0
&& !s.flags.contains(SymbolFlag::Hidden)
&& !s.flags.contains(SymbolFlag::Ignored) && !s.flags.contains(SymbolFlag::Ignored)
}) })
} }
+2 -4
View File
@@ -34,14 +34,12 @@ impl Demangler {
fn demangle_itanium(name: &str) -> Option<String> { fn demangle_itanium(name: &str) -> Option<String> {
let name = name.trim_start_matches('.'); let name = name.trim_start_matches('.');
cpp_demangle::Symbol::new(name) cpp_demangle::Symbol::new(name).ok().and_then(|s| s.demangle().ok())
.ok()
.and_then(|s| s.demangle(&cpp_demangle::DemangleOptions::default()).ok())
} }
fn demangle_gnu_legacy(name: &str) -> Option<String> { fn demangle_gnu_legacy(name: &str) -> Option<String> {
let name = name.trim_start_matches('.'); let name = name.trim_start_matches('.');
gnuv2_demangle::demangle(name, &gnuv2_demangle::DemangleConfig::new_no_cfilt_mimics()).ok() gnuv2_demangle::demangle(name, &gnuv2_demangle::DemangleConfig::new()).ok()
} }
} }
+40 -14
View File
@@ -37,6 +37,8 @@ pub enum DiffText<'a> {
Argument(InstructionArgValue<'a>), Argument(InstructionArgValue<'a>),
/// Branch destination /// Branch destination
BranchDest(u64), BranchDest(u64),
/// Branch source/destination arrow, scrolls to a specific row when clicked
BranchArrow(u32),
/// Symbol name /// Symbol name
Symbol(&'a Symbol), Symbol(&'a Symbol),
/// Relocation addend /// Relocation addend
@@ -185,7 +187,13 @@ pub fn display_row(
pad_to: 5, pad_to: 5,
})?; })?;
if let Some(branch) = &ins_row.branch_from { if let Some(branch) = &ins_row.branch_from {
cb(DiffTextSegment::basic(" ~> ", DiffTextColor::Rotating(branch.branch_idx as u8)))?; // Pick the first branch source if there are multiple.
let ins_idx = branch.ins_idx[0];
cb(DiffTextSegment {
text: DiffText::BranchArrow(ins_idx),
color: DiffTextColor::Rotating(branch.branch_idx as u8),
pad_to: 0,
})?;
} else { } else {
cb(DiffTextSegment::spacing(4))?; cb(DiffTextSegment::spacing(4))?;
} }
@@ -307,7 +315,11 @@ pub fn display_row(
cb(DiffTextSegment::basic(">", base_color))?; cb(DiffTextSegment::basic(">", base_color))?;
} }
if let Some(branch) = &ins_row.branch_to { if let Some(branch) = &ins_row.branch_to {
cb(DiffTextSegment::basic(" ~>", DiffTextColor::Rotating(branch.branch_idx as u8)))?; cb(DiffTextSegment {
text: DiffText::BranchArrow(branch.ins_idx),
color: DiffTextColor::Rotating(branch.branch_idx as u8),
pad_to: 0,
})?;
} }
cb(EOL_SEGMENT)?; cb(EOL_SEGMENT)?;
Ok(()) Ok(())
@@ -354,7 +366,7 @@ impl From<&DiffText<'_>> for HighlightKind {
} }
pub enum ContextItem { pub enum ContextItem {
Copy { value: String, label: Option<String> }, Copy { value: String, label: Option<String>, copy_string: Option<String> },
Navigate { label: String, symbol_index: usize, kind: SymbolNavigationKind }, Navigate { label: String, symbol_index: usize, kind: SymbolNavigationKind },
Separator, Separator,
} }
@@ -386,9 +398,9 @@ pub fn symbol_context(obj: &Object, symbol_index: usize) -> Vec<ContextItem> {
return Vec::new(); return Vec::new();
}; };
let mut out = Vec::new(); let mut out = Vec::new();
out.push(ContextItem::Copy { value: symbol.name.clone(), label: None }); out.push(ContextItem::Copy { value: symbol.name.clone(), label: None, copy_string: None });
if let Some(name) = &symbol.demangled_name { if let Some(name) = &symbol.demangled_name {
out.push(ContextItem::Copy { value: name.clone(), label: None }); out.push(ContextItem::Copy { value: name.clone(), label: None, copy_string: None });
} }
if symbol.section.is_some() if symbol.section.is_some()
&& let Some(address) = symbol.virtual_address && let Some(address) = symbol.virtual_address
@@ -396,6 +408,7 @@ pub fn symbol_context(obj: &Object, symbol_index: usize) -> Vec<ContextItem> {
out.push(ContextItem::Copy { out.push(ContextItem::Copy {
value: format!("{address:x}"), value: format!("{address:x}"),
label: Some("virtual address".to_string()), label: Some("virtual address".to_string()),
copy_string: None,
}); });
} }
out.append(&mut obj.arch.symbol_context(obj, symbol_index)); out.append(&mut obj.arch.symbol_context(obj, symbol_index));
@@ -489,8 +502,8 @@ pub fn relocation_context(
let literals = display_ins_data_literals(obj, ins); let literals = display_ins_data_literals(obj, ins);
if !literals.is_empty() { if !literals.is_empty() {
out.push(ContextItem::Separator); out.push(ContextItem::Separator);
for (literal, label_override) in literals { for (literal, label_override, copy_string) in literals {
out.push(ContextItem::Copy { value: literal, label: label_override }); out.push(ContextItem::Copy { value: literal, label: label_override, copy_string });
} }
} }
} }
@@ -586,24 +599,37 @@ pub fn instruction_context(
for byte in resolved.code { for byte in resolved.code {
hex_string.push_str(&format!("{byte:02x}")); hex_string.push_str(&format!("{byte:02x}"));
} }
out.push(ContextItem::Copy { value: hex_string, label: Some("instruction bytes".to_string()) }); out.push(ContextItem::Copy {
value: hex_string,
label: Some("instruction bytes".to_string()),
copy_string: None,
});
out.append(&mut obj.arch.instruction_context(obj, resolved)); out.append(&mut obj.arch.instruction_context(obj, resolved));
if let Some(virtual_address) = resolved.symbol.virtual_address { if let Some(virtual_address) = resolved.symbol.virtual_address {
let offset = resolved.ins_ref.address - resolved.symbol.address; let offset = resolved.ins_ref.address - resolved.symbol.address;
out.push(ContextItem::Copy { out.push(ContextItem::Copy {
value: format!("{:x}", virtual_address + offset), value: format!("{:x}", virtual_address + offset),
label: Some("virtual address".to_string()), label: Some("virtual address".to_string()),
copy_string: None,
}); });
} }
for arg in &ins.args { for arg in &ins.args {
if let InstructionArg::Value(arg) = arg { if let InstructionArg::Value(arg) = arg {
out.push(ContextItem::Copy { value: arg.to_string(), label: None }); out.push(ContextItem::Copy { value: arg.to_string(), label: None, copy_string: None });
match arg { match arg {
InstructionArgValue::Signed(v) => { InstructionArgValue::Signed(v) => {
out.push(ContextItem::Copy { value: v.to_string(), label: None }); out.push(ContextItem::Copy {
value: v.to_string(),
label: None,
copy_string: None,
});
} }
InstructionArgValue::Unsigned(v) => { InstructionArgValue::Unsigned(v) => {
out.push(ContextItem::Copy { value: v.to_string(), label: None }); out.push(ContextItem::Copy {
value: v.to_string(),
label: None,
copy_string: None,
});
} }
_ => {} _ => {}
} }
@@ -665,10 +691,10 @@ pub fn instruction_hover(
let literals = display_ins_data_literals(obj, resolved); let literals = display_ins_data_literals(obj, resolved);
if !literals.is_empty() { if !literals.is_empty() {
out.push(HoverItem::Separator); out.push(HoverItem::Separator);
for (literal, label_override) in literals { for (literal, label_override, _) in literals {
out.push(HoverItem::Text { out.push(HoverItem::Text {
label: label_override.unwrap_or_else(|| ty.to_string()), label: label_override.unwrap_or_else(|| ty.to_string()),
value: literal, value: format!("{literal:?}"),
color: HoverItemColor::Normal, color: HoverItemColor::Normal,
}); });
} }
@@ -859,7 +885,7 @@ pub fn display_ins_data_labels(obj: &Object, resolved: ResolvedInstructionRef) -
pub fn display_ins_data_literals( pub fn display_ins_data_literals(
obj: &Object, obj: &Object,
resolved: ResolvedInstructionRef, resolved: ResolvedInstructionRef,
) -> Vec<(String, Option<String>)> { ) -> Vec<(String, Option<String>, Option<String>)> {
let Some(reloc) = resolved.relocation else { let Some(reloc) = resolved.relocation else {
return Vec::new(); return Vec::new();
}; };
+11 -12
View File
@@ -7,6 +7,7 @@ use alloc::{
use core::{num::NonZeroU32, ops::Range}; use core::{num::NonZeroU32, ops::Range};
use anyhow::Result; use anyhow::Result;
use itertools::Itertools;
use crate::{ use crate::{
diff::{ diff::{
@@ -759,18 +760,16 @@ fn find_symbol(
return closest_match_symbol_idx; return closest_match_symbol_idx;
} }
// Try to find an exact name match // Try to find a symbol with a matching name
if let Some((symbol_idx, _)) = unmatched_symbols(obj, used).find(|(_, symbol)| { if let Some((symbol_idx, _)) = unmatched_symbols(obj, used)
symbol.name == in_symbol.name && symbol_section_kind(obj, symbol) == section_kind .filter(|&(_, symbol)| {
}) { symbol_name_matches(&in_symbol.name, &symbol.name)
return Some(symbol_idx); && symbol_section_kind(obj, symbol) == section_kind
} && symbol_section(obj, symbol).is_some_and(|(name, _)| name == section_name)
})
if let Some((symbol_idx, _)) = unmatched_symbols(obj, used).find(|&(_, symbol)| { .sorted_unstable_by_key(|&(_, symbol)| (symbol.section, symbol.address))
symbol_name_matches(&in_symbol.name, &symbol.name) .next()
&& symbol_section_kind(obj, symbol) == section_kind {
&& symbol_section(obj, symbol).is_some_and(|(name, _)| name == section_name)
}) {
return Some(symbol_idx); return Some(symbol_idx);
} }
+7 -8
View File
@@ -824,19 +824,16 @@ fn combine_sections(
config: &DiffObjConfig, config: &DiffObjConfig,
) -> Result<()> { ) -> Result<()> {
let mut data_sections = BTreeMap::<String, Vec<usize>>::new(); let mut data_sections = BTreeMap::<String, Vec<usize>>::new();
let mut text_sections = Vec::<usize>::new(); let mut text_sections = BTreeMap::<String, Vec<usize>>::new();
for (i, section) in sections.iter().enumerate() { for (i, section) in sections.iter().enumerate() {
let base_name =
if let Some(i) = section.name.rfind('$') { &section.name[..i] } else { &section.name };
match section.kind { match section.kind {
SectionKind::Data | SectionKind::Bss => { SectionKind::Data | SectionKind::Bss => {
let base_name = if let Some(i) = section.name.rfind('$') {
&section.name[..i]
} else {
&section.name
};
data_sections.entry(base_name.to_string()).or_default().push(i); data_sections.entry(base_name.to_string()).or_default().push(i);
} }
SectionKind::Code => { SectionKind::Code => {
text_sections.push(i); text_sections.entry(base_name.to_string()).or_default().push(i);
} }
_ => {} _ => {}
} }
@@ -847,7 +844,9 @@ fn combine_sections(
} }
} }
if config.combine_text_sections { if config.combine_text_sections {
do_combine_sections(sections, symbols, &mut text_sections, ".text".to_string())?; for (combined_name, mut section_indices) in text_sections {
do_combine_sections(sections, symbols, &mut section_indices, combined_name)?;
}
} }
Ok(()) Ok(())
} }
@@ -2,6 +2,6 @@
source: objdiff-core/tests/arch_arm.rs source: objdiff-core/tests/arch_arm.rs
expression: output expression: output
--- ---
[(Line(90), Dim, 5), (Address(0), Dim, 5), (Spacing(4), Normal, 0), (Opcode("ldr", 32799), Normal, 10), (Argument(Opaque("r12")), Normal, 0), (Basic(", "), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("pc")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(0)), Normal, 0), (Basic("]"), Normal, 0), (Basic(" (->"), Normal, 0), (BranchDest(8), Normal, 0), (Basic(")"), Normal, 0), (Eol, Normal, 0)] [(Line(90), Dim, 5), (Address(0), Dim, 5), (Spacing(4), Normal, 0), (Opcode("ldr", 32792), Normal, 10), (Argument(Opaque("r12")), Normal, 0), (Basic(", "), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("pc")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(0)), Normal, 0), (Basic("]"), Normal, 0), (Basic(" (->"), Normal, 0), (BranchDest(8), Normal, 0), (Basic(")"), Normal, 0), (Eol, Normal, 0)]
[(Line(90), Dim, 5), (Address(4), Dim, 5), (Spacing(4), Normal, 0), (Opcode("bx", 32779), Normal, 10), (Argument(Opaque("r12")), Normal, 0), (Eol, Normal, 0)] [(Line(90), Dim, 5), (Address(4), Dim, 5), (Spacing(4), Normal, 0), (Opcode("bx", 32777), Normal, 10), (Argument(Opaque("r12")), Normal, 0), (Eol, Normal, 0)]
[(Line(90), Dim, 5), (Address(8), Dim, 5), (Spacing(4), Normal, 0), (Opcode(".word", 65534), Normal, 10), (Symbol(Symbol { name: "esEnemyDraw", demangled_name: None, address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global), align: None, virtual_address: None }), Bright, 0), (Eol, Normal, 0)] [(Line(90), Dim, 5), (Address(8), Dim, 5), (Spacing(4), Normal, 0), (Opcode(".word", 65534), Normal, 10), (Symbol(Symbol { name: "esEnemyDraw", demangled_name: None, address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global), align: None, virtual_address: None }), Bright, 0), (Eol, Normal, 0)]
@@ -8,7 +8,7 @@ expression: diff.instruction_rows
InstructionRef { InstructionRef {
address: 76, address: 76,
size: 4, size: 4,
opcode: 32799, opcode: 32792,
branch_dest: None, branch_dest: None,
}, },
), ),
@@ -22,7 +22,7 @@ expression: diff.instruction_rows
InstructionRef { InstructionRef {
address: 80, address: 80,
size: 4, size: 4,
opcode: 32779, opcode: 32777,
branch_dest: None, branch_dest: None,
}, },
), ),
File diff suppressed because it is too large Load Diff

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