Compare commits

...

18 Commits
v3.4.0 ... main

Author SHA1 Message Date
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
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
26a4cc79cf Version 3.4.3 2025-11-22 12:47:03 -07:00
5c96c2ee51 Update cwextab 2025-11-22 12:45:58 -07:00
d162fe847e Remove --mapping, --selecting-{left,right} from CLI 2025-11-21 23:55:26 -07:00
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
03a578c1bb Update test snapshots for new cwextab 2025-11-18 22:29:42 -07:00
d09ef8e207 Version v3.4.1 2025-11-18 22:22:43 -07:00
63552a58ae Move copy button to first header row 2025-11-18 22:22:02 -07:00
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
21 changed files with 923 additions and 834 deletions

1476
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -14,7 +14,7 @@ default-members = [
resolver = "3" resolver = "3"
[workspace.package] [workspace.package]
version = "3.4.0" version = "3.4.5"
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"

View File

@@ -75,6 +75,7 @@ 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" },
] ]
# 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.

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"

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 {

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

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 = "2.0", 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,15 +169,15 @@ 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.4", optional = true } gnuv2_demangle = { version = "0.4", optional = true }
msvc-demangler = { version = "0.11", optional = true } msvc-demangler = { version = "0.11", optional = true }

View File

@@ -225,8 +225,13 @@ impl Arch for ArchArm {
// Check how many bytes we can/should read // Check how many bytes we can/should read
let num_code_bytes = if data.len() >= 4 { let num_code_bytes = if data.len() >= 4 {
// Read 4 bytes even for Thumb, as the parser will determine if it's a 2 or 4 byte instruction if mode == unarm::ParseMode::Data && address & 3 != 0 {
4 // 32-bit .word value should be aligned on a 4-byte boundary, otherwise use .hword
2
} else {
// Read 4 bytes even for Thumb, as the parser will determine if it's a 2 or 4 byte instruction
4
}
} else if mode != unarm::ParseMode::Arm { } else if mode != unarm::ParseMode::Arm {
2 2
} else { } else {
@@ -340,7 +345,10 @@ impl Arch for ArchArm {
let address = address as usize; let address = address as usize;
let addend = match r_type { let addend = match r_type {
// ARM calls // ARM calls
elf::R_ARM_PC24 | elf::R_ARM_XPC25 | elf::R_ARM_CALL => { elf::R_ARM_PC24
| elf::R_ARM_XPC25
| elf::R_ARM_CALL
| elf::R_ARM_JUMP24 => {
let data = section_data[address..address + 4].try_into()?; let data = section_data[address..address + 4].try_into()?;
let addend = self.endianness.read_i32_bytes(data); let addend = self.endianness.read_i32_bytes(data);
let imm24 = addend & 0xffffff; let imm24 = addend & 0xffffff;

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 {
@@ -77,7 +85,7 @@ impl DataType {
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
} }
@@ -164,16 +172,18 @@ impl DataType {
strs.push((format!("{bytes:#?}"), None)); strs.push((format!("{bytes:#?}"), 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) { strs.push((format!("{cow}"), Some("ASCII".into())));
strs.push((str, Some("Shift JIS".into()))); }
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()) {
strs.push((format!("{cow}"), Some(encoding_name.into())));
} }
} }
} }

View File

@@ -37,12 +37,13 @@ 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 // Match Metrowerks symbol$1234 against symbol$2345
if let Some((prefix, suffix)) = left_name.split_once('$') { // and GCC symbol.1234 against symbol.2345
if let Some((prefix, suffix)) = left_name.split_once(['$', '.']) {
if !suffix.chars().all(char::is_numeric) { if !suffix.chars().all(char::is_numeric) {
return false; return false;
} }
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 +588,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)
}) })
} }

View File

@@ -34,9 +34,7 @@ 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> {

View File

@@ -668,7 +668,7 @@ pub fn instruction_hover(
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,
}); });
} }

View File

@@ -30,9 +30,9 @@ cfg-if = "1.0"
const_format = "0.2" const_format = "0.2"
cwdemangle = "1.0" cwdemangle = "1.0"
dirs = "6.0" dirs = "6.0"
egui = "0.32" egui = "0.33"
egui_extras = "0.32" egui_extras = "0.33"
egui-notify = "0.20" egui-notify = "0.21"
filetime = "0.2" filetime = "0.2"
float-ord = "0.3" float-ord = "0.3"
font-kit = "0.14" font-kit = "0.14"
@@ -40,21 +40,21 @@ globset = { version = "0.4", features = ["serde1"] }
log = "0.4" log = "0.4"
objdiff-core = { path = "../objdiff-core", features = ["all"] } objdiff-core = { path = "../objdiff-core", features = ["all"] }
open = "5.3" open = "5.3"
png = "0.17" png = "0.18"
pollster = "0.4" pollster = "0.4"
regex = "1.11" regex = "1.12"
rfd = { version = "0.15" } #, default-features = false, features = ['xdg-portal'] rfd = { version = "0.15" } #, default-features = false, features = ['xdg-portal']
rlwinmdec = "1.1" rlwinmdec = "1.1"
ron = "0.10" ron = "0.12"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
time = { version = "0.3", features = ["formatting", "local-offset"] } time = { version = "0.3", features = ["formatting", "local-offset"] }
typed-path = "0.11" typed-path = "0.12"
winit = { version = "0.30", features = ["default"] } winit = { version = "0.30", features = ["default"] }
tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] }
# Keep version in sync with egui # Keep version in sync with egui
[dependencies.eframe] [dependencies.eframe]
version = "0.32" version = "0.33"
features = [ features = [
"default_fonts", "default_fonts",
"glow", "glow",
@@ -66,7 +66,7 @@ default-features = false
# Keep version in sync with eframe # Keep version in sync with eframe
[dependencies.wgpu] [dependencies.wgpu]
version = "25.0" version = "27.0"
features = [ features = [
"dx12", "dx12",
"metal", "metal",

View File

@@ -14,6 +14,7 @@ mod views;
use std::{ use std::{
ffi::OsStr, ffi::OsStr,
fmt::Display, fmt::Display,
io::Cursor,
path::PathBuf, path::PathBuf,
process::ExitCode, process::ExitCode,
rc::Rc, rc::Rc,
@@ -21,7 +22,7 @@ use std::{
sync::{Arc, Mutex}, sync::{Arc, Mutex},
}; };
use anyhow::{Result, ensure}; use anyhow::{Context, Result, ensure};
use argp::{FromArgValue, FromArgs}; use argp::{FromArgValue, FromArgs};
use cfg_if::cfg_if; use cfg_if::cfg_if;
use objdiff_core::config::path::check_path_buf; use objdiff_core::config::path::check_path_buf;
@@ -90,9 +91,9 @@ struct TopLevel {
} }
fn load_icon() -> Result<egui::IconData> { fn load_icon() -> Result<egui::IconData> {
let decoder = png::Decoder::new(include_bytes!("../assets/icon_64.png").as_ref()); let decoder = png::Decoder::new(Cursor::new(include_bytes!("../assets/icon_64.png").as_ref()));
let mut reader = decoder.read_info()?; let mut reader = decoder.read_info()?;
let mut buf = vec![0; reader.output_buffer_size()]; let mut buf = vec![0; reader.output_buffer_size().context("Buffer too large")?];
let info = reader.next_frame(&mut buf)?; let info = reader.next_frame(&mut buf)?;
ensure!(info.bit_depth == png::BitDepth::Eight); ensure!(info.bit_depth == png::BitDepth::Eight);
ensure!(info.color_type == png::ColorType::Rgba); ensure!(info.color_type == png::ColorType::Rgba);

View File

@@ -1,12 +1,18 @@
use std::cmp::Ordering;
use egui::{Id, Layout, RichText, ScrollArea, TextEdit, Ui, Widget, text::LayoutJob}; use egui::{Id, Layout, RichText, ScrollArea, TextEdit, Ui, Widget, text::LayoutJob};
use objdiff_core::{ use objdiff_core::{
build::BuildStatus, build::BuildStatus,
diff::{ diff::{
DiffObjConfig, ObjectDiff, SymbolDiff, DiffObjConfig, ObjectDiff, SymbolDiff,
data::BYTES_PER_ROW, data::BYTES_PER_ROW,
display::{ContextItem, HoverItem, HoverItemColor, SymbolFilter, SymbolNavigationKind}, display::{
ContextItem, DiffText, HoverItem, HoverItemColor, SymbolFilter, SymbolNavigationKind,
display_row,
},
}, },
obj::{Object, Symbol}, obj::{InstructionArgValue, Object, Symbol},
util::ReallySigned,
}; };
use time::format_description; use time::format_description;
@@ -64,6 +70,51 @@ impl<'a> DiffColumnContext<'a> {
pub fn id(&self) -> Option<&str> { self.symbol.map(|(symbol, _, _)| symbol.name.as_str()) } pub fn id(&self) -> Option<&str> { self.symbol.map(|(symbol, _, _)| symbol.name.as_str()) }
} }
/// Obtains the assembly text for a given symbol diff, suitable for copying to clipboard.
fn get_asm_text(
obj: &Object,
symbol_diff: &SymbolDiff,
symbol_idx: usize,
diff_config: &DiffObjConfig,
) -> String {
let mut asm_text = String::new();
for ins_row in &symbol_diff.instruction_rows {
let mut line = String::new();
let result = display_row(obj, symbol_idx, ins_row, diff_config, |segment| {
let text = match segment.text {
DiffText::Basic(text) => text.to_string(),
DiffText::Line(num) => format!("{num} "),
DiffText::Address(addr) => format!("{addr:x}:"),
DiffText::Opcode(mnemonic, _op) => format!("{mnemonic} "),
DiffText::Argument(arg) => match arg {
InstructionArgValue::Signed(v) => format!("{:#x}", ReallySigned(v)),
InstructionArgValue::Unsigned(v) => format!("{v:#x}"),
InstructionArgValue::Opaque(v) => v.into_owned(),
},
DiffText::BranchDest(addr) => format!("{addr:x}"),
DiffText::Symbol(sym) => sym.demangled_name.as_ref().unwrap_or(&sym.name).clone(),
DiffText::Addend(addend) => match addend.cmp(&0i64) {
Ordering::Greater => format!("+{addend:#x}"),
Ordering::Less => format!("-{:#x}", -addend),
_ => String::new(),
},
DiffText::Spacing(n) => " ".repeat(n.into()),
DiffText::Eol => "\n".to_string(),
};
line.push_str(&text);
Ok(())
});
if result.is_ok() {
asm_text.push_str(line.trim_end());
asm_text.push('\n');
}
}
asm_text
}
#[must_use] #[must_use]
pub fn diff_view_ui( pub fn diff_view_ui(
ui: &mut Ui, ui: &mut Ui,
@@ -162,6 +213,17 @@ pub fn diff_view_ui(
ret = Some(DiffViewAction::CreateScratch(symbol.name.clone())); ret = Some(DiffViewAction::CreateScratch(symbol.name.clone()));
} }
} }
if state.current_view == View::FunctionDiff
&& let Some((_, symbol_diff, symbol_idx)) = left_ctx.symbol
&& let Some((obj, _)) = left_ctx.obj
&& ui
.button("📋 Copy")
.on_hover_text_at_pointer("Copy all rows to clipboard")
.clicked()
{
ui.ctx().copy_text(get_asm_text(obj, symbol_diff, symbol_idx, diff_config));
}
}); });
} }
@@ -208,16 +270,20 @@ pub fn diff_view_ui(
// Third row // Third row
if left_ctx.has_symbol() && right_ctx.has_symbol() { if left_ctx.has_symbol() && right_ctx.has_symbol() {
if (state.current_view == View::FunctionDiff ui.horizontal(|ui| {
&& ui if (state.current_view == View::FunctionDiff
.button("Change target") && ui
.on_hover_text_at_pointer("Choose a different symbol to use as the target") .button("Change target")
.clicked() .on_hover_text_at_pointer(
|| hotkeys::consume_change_target_shortcut(ui.ctx())) "Choose a different symbol to use as the target",
&& let Some(symbol_ref) = state.symbol_state.right_symbol.as_ref() )
{ .clicked()
ret = Some(DiffViewAction::SelectingLeft(symbol_ref.clone())); || hotkeys::consume_change_target_shortcut(ui.ctx()))
} && let Some(symbol_ref) = state.symbol_state.right_symbol.as_ref()
{
ret = Some(DiffViewAction::SelectingLeft(symbol_ref.clone()));
}
});
} else if left_ctx.status.success && !left_ctx.has_symbol() { } else if left_ctx.status.success && !left_ctx.has_symbol() {
ui.horizontal(|ui| { ui.horizontal(|ui| {
let mut search = state.search.clone(); let mut search = state.search.clone();
@@ -291,6 +357,15 @@ pub fn diff_view_ui(
{ {
ret = Some(DiffViewAction::OpenSourcePath); ret = Some(DiffViewAction::OpenSourcePath);
} }
if let Some((_, symbol_diff, symbol_idx)) = right_ctx.symbol
&& let Some((obj, _)) = right_ctx.obj
&& ui
.button("📋 Copy")
.on_hover_text_at_pointer("Copy all rows to clipboard")
.clicked()
{
ui.ctx().copy_text(get_asm_text(obj, symbol_diff, symbol_idx, diff_config));
}
}); });
// Second row // Second row
@@ -796,19 +871,13 @@ pub fn context_menu_items_ui(
match item { match item {
ContextItem::Copy { value, label } => { ContextItem::Copy { value, label } => {
let mut job = LayoutJob::default(); let mut job = LayoutJob::default();
write_text("Copy ", appearance.text_color, &mut job, appearance.code_font.clone());
write_text( write_text(
"Copy \"", &format!("{value:?}"),
appearance.text_color,
&mut job,
appearance.code_font.clone(),
);
write_text(
&value,
appearance.highlight_color, appearance.highlight_color,
&mut job, &mut job,
appearance.code_font.clone(), appearance.code_font.clone(),
); );
write_text("\"", appearance.text_color, &mut job, appearance.code_font.clone());
if let Some(label) = label { if let Some(label) = label {
write_text(" (", appearance.text_color, &mut job, appearance.code_font.clone()); write_text(" (", appearance.text_color, &mut job, appearance.code_font.clone());
write_text( write_text(

View File

@@ -104,7 +104,7 @@ impl FrameHistory {
)); ));
let cpu_usage = to_screen.inverse().transform_pos(pointer_pos).y; let cpu_usage = to_screen.inverse().transform_pos(pointer_pos).y;
let text = format!("{:.1} ms", 1e3 * cpu_usage); let text = format!("{:.1} ms", 1e3 * cpu_usage);
shapes.push(ui.fonts(|f| { shapes.push(ui.fonts_mut(|f| {
Shape::text( Shape::text(
f, f,
pos2(rect.left(), y), pos2(rect.left(), y),

View File

@@ -217,7 +217,7 @@ fn asm_row_ui(
if ins_diff.kind != InstructionDiffKind::None { if ins_diff.kind != InstructionDiffKind::None {
ui.painter().rect_filled(ui.available_rect_before_wrap(), 0.0, ui.visuals().faint_bg_color); 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, ' ')); let space_width = ui.fonts_mut(|f| f.glyph_width(&appearance.code_font, ' '));
display_row(obj, symbol_idx, ins_diff, diff_config, |segment| { display_row(obj, symbol_idx, ins_diff, diff_config, |segment| {
if let Some(action) = if let Some(action) =
diff_text_ui(ui, segment, appearance, ins_view_state, column, space_width, &response_cb) diff_text_ui(ui, segment, appearance, ins_view_state, column, space_width, &response_cb)

View File

@@ -22,8 +22,8 @@ std = ["objdiff-core/std"]
[dependencies] [dependencies]
log = { version = "0.4", default-features = false } log = { version = "0.4", default-features = false }
regex = { version = "1.11", default-features = false, features = ["unicode-case"] } regex = { version = "1.12", default-features = false, features = ["unicode-case"] }
wit-bindgen = { version = "0.44", default-features = false, features = ["macros"] } wit-bindgen = { version = "0.48", default-features = false, features = ["macros"] }
xxhash-rust = { version = "0.8", default-features = false, features = ["xxh3"] } xxhash-rust = { version = "0.8", default-features = false, features = ["xxh3"] }
[dependencies.objdiff-core] [dependencies.objdiff-core]

View File

@@ -1,12 +1,12 @@
{ {
"name": "objdiff-wasm", "name": "objdiff-wasm",
"version": "3.4.0", "version": "3.4.5",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "objdiff-wasm", "name": "objdiff-wasm",
"version": "3.4.0", "version": "3.4.5",
"license": "MIT OR Apache-2.0", "license": "MIT OR Apache-2.0",
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^1.9.3", "@biomejs/biome": "^1.9.3",
@@ -852,7 +852,6 @@
"integrity": "sha512-Y3b4Y7lGIvNPywspP38deHkp/EEkTXrJEHeX1K5yz8U/94PkWvPlDebjjiSvmI6TT+9iVzsq22qDlBEdDuJZhA==", "integrity": "sha512-Y3b4Y7lGIvNPywspP38deHkp/EEkTXrJEHeX1K5yz8U/94PkWvPlDebjjiSvmI6TT+9iVzsq22qDlBEdDuJZhA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@rspack/core": "1.2.3", "@rspack/core": "1.2.3",
"@rspack/lite-tapable": "~1.0.1", "@rspack/lite-tapable": "~1.0.1",
@@ -1084,7 +1083,6 @@
"integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"dependencies": { "dependencies": {
"tslib": "^2.8.0" "tslib": "^2.8.0"
} }
@@ -2141,7 +2139,6 @@
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -2169,7 +2166,6 @@
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"

View File

@@ -1,6 +1,6 @@
{ {
"name": "objdiff-wasm", "name": "objdiff-wasm",
"version": "3.4.0", "version": "3.4.5",
"description": "A local diffing tool for decompilation projects.", "description": "A local diffing tool for decompilation projects.",
"author": { "author": {
"name": "Luke Street", "name": "Luke Street",

View File

@@ -469,16 +469,17 @@ impl From<obj::SymbolFlagSet> for SymbolFlags {
fn from(flags: obj::SymbolFlagSet) -> SymbolFlags { fn from(flags: obj::SymbolFlagSet) -> SymbolFlags {
let mut out = SymbolFlags::empty(); let mut out = SymbolFlags::empty();
for flag in flags { for flag in flags {
out |= match flag { out = out
obj::SymbolFlag::Global => SymbolFlags::GLOBAL, | match flag {
obj::SymbolFlag::Local => SymbolFlags::LOCAL, obj::SymbolFlag::Global => SymbolFlags::GLOBAL,
obj::SymbolFlag::Weak => SymbolFlags::WEAK, obj::SymbolFlag::Local => SymbolFlags::LOCAL,
obj::SymbolFlag::Common => SymbolFlags::COMMON, obj::SymbolFlag::Weak => SymbolFlags::WEAK,
obj::SymbolFlag::Hidden => SymbolFlags::HIDDEN, obj::SymbolFlag::Common => SymbolFlags::COMMON,
obj::SymbolFlag::HasExtra => SymbolFlags::HAS_EXTRA, obj::SymbolFlag::Hidden => SymbolFlags::HIDDEN,
obj::SymbolFlag::SizeInferred => SymbolFlags::SIZE_INFERRED, obj::SymbolFlag::HasExtra => SymbolFlags::HAS_EXTRA,
obj::SymbolFlag::Ignored => SymbolFlags::IGNORED, obj::SymbolFlag::SizeInferred => SymbolFlags::SIZE_INFERRED,
}; obj::SymbolFlag::Ignored => SymbolFlags::IGNORED,
};
} }
out out
} }