Merge branch 'main' into arm

This commit is contained in:
Aetias 2024-05-23 16:46:02 +02:00
commit 0f9dd7ed41
23 changed files with 689 additions and 219 deletions

8
Cargo.lock generated
View File

@ -3087,7 +3087,7 @@ dependencies = [
[[package]] [[package]]
name = "objdiff-cli" name = "objdiff-cli"
version = "0.1.0" version = "2.0.0-alpha.2"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"argp", "argp",
@ -3106,7 +3106,7 @@ dependencies = [
[[package]] [[package]]
name = "objdiff-core" name = "objdiff-core"
version = "1.0.0" version = "2.0.0-alpha.2"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"armv5te", "armv5te",
@ -3130,11 +3130,12 @@ dependencies = [
"serde_json", "serde_json",
"serde_yaml", "serde_yaml",
"similar", "similar",
"strum",
] ]
[[package]] [[package]]
name = "objdiff-gui" name = "objdiff-gui"
version = "1.0.0" version = "2.0.0-alpha.2"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@ -3164,6 +3165,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"shell-escape", "shell-escape",
"strum",
"tempfile", "tempfile",
"time", "time",
"tracing-subscriber", "tracing-subscriber",

View File

@ -14,8 +14,8 @@ Features:
- Click to highlight all instances of values and registers. - Click to highlight all instances of values and registers.
Supports: Supports:
- PowerPC 750CL (GameCube & Wii) - PowerPC 750CL (GameCube, Wii)
- MIPS (Nintendo 64 & PS2) - MIPS (N64, PS1, PS2, PSP)
- x86 (COFF only at the moment) - x86 (COFF only at the moment)
See [Usage](#usage) for more information. See [Usage](#usage) for more information.

View File

@ -1,6 +1,6 @@
[package] [package]
name = "objdiff-cli" name = "objdiff-cli"
version = "0.1.0" version = "2.0.0-alpha.2"
edition = "2021" edition = "2021"
rust-version = "1.70" rust-version = "1.70"
authors = ["Luke Street <luke@street.dev>"] authors = ["Luke Street <luke@street.dev>"]

View File

@ -823,6 +823,8 @@ impl FunctionDiffUi {
relax_reloc_diffs: self.relax_reloc_diffs, relax_reloc_diffs: self.relax_reloc_diffs,
space_between_args: true, // TODO space_between_args: true, // TODO
x86_formatter: Default::default(), // TODO x86_formatter: Default::default(), // TODO
mips_abi: Default::default(), // TODO
mips_instr_category: Default::default(), // TODO
}; };
let result = diff::diff_objs(&config, target.as_ref(), base.as_ref(), prev.as_ref())?; let result = diff::diff_objs(&config, target.as_ref(), base.as_ref(), prev.as_ref())?;

View File

@ -42,7 +42,7 @@ pub struct GenerateArgs {
/// Output JSON file /// Output JSON file
output: Option<PathBuf>, output: Option<PathBuf>,
#[argp(switch, short = 'd')] #[argp(switch, short = 'd')]
/// Deduplicate global and weak symbols /// Deduplicate global and weak symbols (runs single-threaded)
deduplicate: bool, deduplicate: bool,
} }
@ -64,9 +64,12 @@ pub struct ChangesArgs {
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
struct Report { struct Report {
fuzzy_match_percent: f32, fuzzy_match_percent: f32,
total_size: u64, total_code: u64,
matched_size: u64, matched_code: u64,
matched_size_percent: f32, matched_code_percent: f32,
total_data: u64,
matched_data: u64,
matched_data_percent: f32,
total_functions: u32, total_functions: u32,
matched_functions: u32, matched_functions: u32,
matched_functions_percent: f32, matched_functions_percent: f32,
@ -77,8 +80,10 @@ struct Report {
struct ReportUnit { struct ReportUnit {
name: String, name: String,
fuzzy_match_percent: f32, fuzzy_match_percent: f32,
total_size: u64, total_code: u64,
matched_size: u64, matched_code: u64,
total_data: u64,
matched_data: u64,
total_functions: u32, total_functions: u32,
matched_functions: u32, matched_functions: u32,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
@ -87,11 +92,12 @@ struct ReportUnit {
module_name: Option<String>, module_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
module_id: Option<u32>, module_id: Option<u32>,
functions: Vec<ReportFunction>, sections: Vec<ReportItem>,
functions: Vec<ReportItem>,
} }
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
struct ReportFunction { struct ReportItem {
name: String, name: String,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
demangled_name: Option<String>, demangled_name: Option<String>,
@ -160,21 +166,29 @@ fn generate(args: GenerateArgs) -> Result<()> {
report.units = units.into_iter().flatten().collect(); report.units = units.into_iter().flatten().collect();
} }
for unit in &report.units { for unit in &report.units {
report.fuzzy_match_percent += unit.fuzzy_match_percent * unit.total_size as f32; report.fuzzy_match_percent += unit.fuzzy_match_percent * unit.total_code as f32;
report.total_size += unit.total_size; report.total_code += unit.total_code;
report.matched_size += unit.matched_size; report.matched_code += unit.matched_code;
report.total_data += unit.total_data;
report.matched_data += unit.matched_data;
report.total_functions += unit.total_functions; report.total_functions += unit.total_functions;
report.matched_functions += unit.matched_functions; report.matched_functions += unit.matched_functions;
} }
if report.total_size == 0 { if report.total_code == 0 {
report.fuzzy_match_percent = 100.0; report.fuzzy_match_percent = 100.0;
} else { } else {
report.fuzzy_match_percent /= report.total_size as f32; report.fuzzy_match_percent /= report.total_code as f32;
} }
report.matched_size_percent = if report.total_size == 0 {
report.matched_code_percent = if report.total_code == 0 {
100.0 100.0
} else { } else {
report.matched_size as f32 / report.total_size as f32 * 100.0 report.matched_code as f32 / report.total_code as f32 * 100.0
};
report.matched_data_percent = if report.total_data == 0 {
100.0
} else {
report.matched_data as f32 / report.total_data as f32 * 100.0
}; };
report.matched_functions_percent = if report.total_functions == 0 { report.matched_functions_percent = if report.total_functions == 0 {
100.0 100.0
@ -216,7 +230,6 @@ fn report_object(
} }
_ => {} _ => {}
} }
// println!("Checking {}", object.name());
let target = object let target = object
.target_path .target_path
.as_ref() .as_ref()
@ -240,11 +253,37 @@ fn report_object(
..Default::default() ..Default::default()
}; };
let obj = target.as_ref().or(base.as_ref()).unwrap(); let obj = target.as_ref().or(base.as_ref()).unwrap();
let obj_diff = result.left.as_ref().or(result.right.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) { for (section, section_diff) in obj.sections.iter().zip(&obj_diff.sections) {
if section.kind != ObjSectionKind::Code { 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 == Some(true) {
100.0
} else {
0.0
}
});
unit.sections.push(ReportItem {
name: section.name.clone(),
demangled_name: None,
fuzzy_match_percent: section_match_percent,
size: section.size,
address: section.virtual_address,
});
match section.kind {
ObjSectionKind::Data | ObjSectionKind::Bss => {
unit.total_data += section.size;
if section_match_percent == 100.0 {
unit.matched_data += section.size;
}
continue; continue;
} }
ObjSectionKind::Code => (),
}
for (symbol, symbol_diff) in section.symbols.iter().zip(&section_diff.symbols) { for (symbol, symbol_diff) in section.symbols.iter().zip(&section_diff.symbols) {
if symbol.size == 0 { if symbol.size == 0 {
continue; continue;
@ -267,11 +306,11 @@ fn report_object(
} }
}); });
unit.fuzzy_match_percent += match_percent * symbol.size as f32; unit.fuzzy_match_percent += match_percent * symbol.size as f32;
unit.total_size += symbol.size; unit.total_code += symbol.size;
if match_percent == 100.0 { if match_percent == 100.0 {
unit.matched_size += symbol.size; unit.matched_code += symbol.size;
} }
unit.functions.push(ReportFunction { unit.functions.push(ReportItem {
name: symbol.name.clone(), name: symbol.name.clone(),
demangled_name: symbol.demangled_name.clone(), demangled_name: symbol.demangled_name.clone(),
size: symbol.size, size: symbol.size,
@ -284,10 +323,10 @@ fn report_object(
unit.total_functions += 1; unit.total_functions += 1;
} }
} }
if unit.total_size == 0 { if unit.total_code == 0 {
unit.fuzzy_match_percent = 100.0; unit.fuzzy_match_percent = 100.0;
} else { } else {
unit.fuzzy_match_percent /= unit.total_size as f32; unit.fuzzy_match_percent /= unit.total_code as f32;
} }
Ok(Some(unit)) Ok(Some(unit))
} }
@ -302,9 +341,12 @@ struct Changes {
#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
struct ChangeInfo { struct ChangeInfo {
fuzzy_match_percent: f32, fuzzy_match_percent: f32,
total_size: u64, total_code: u64,
matched_size: u64, matched_code: u64,
matched_size_percent: f32, matched_code_percent: f32,
total_data: u64,
matched_data: u64,
matched_data_percent: f32,
total_functions: u32, total_functions: u32,
matched_functions: u32, matched_functions: u32,
matched_functions_percent: f32, matched_functions_percent: f32,
@ -314,9 +356,12 @@ impl From<&Report> for ChangeInfo {
fn from(report: &Report) -> Self { fn from(report: &Report) -> Self {
Self { Self {
fuzzy_match_percent: report.fuzzy_match_percent, fuzzy_match_percent: report.fuzzy_match_percent,
total_size: report.total_size, total_code: report.total_code,
matched_size: report.matched_size, matched_code: report.matched_code,
matched_size_percent: report.matched_size_percent, matched_code_percent: report.matched_code_percent,
total_data: report.total_data,
matched_data: report.matched_data,
matched_data_percent: report.matched_data_percent,
total_functions: report.total_functions, total_functions: report.total_functions,
matched_functions: report.matched_functions, matched_functions: report.matched_functions,
matched_functions_percent: report.matched_functions_percent, matched_functions_percent: report.matched_functions_percent,
@ -328,12 +373,19 @@ impl From<&ReportUnit> for ChangeInfo {
fn from(value: &ReportUnit) -> Self { fn from(value: &ReportUnit) -> Self {
Self { Self {
fuzzy_match_percent: value.fuzzy_match_percent, fuzzy_match_percent: value.fuzzy_match_percent,
total_size: value.total_size, total_code: value.total_code,
matched_size: value.matched_size, matched_code: value.matched_code,
matched_size_percent: if value.total_size == 0 { matched_code_percent: if value.total_code == 0 {
100.0 100.0
} else { } else {
value.matched_size as f32 / value.total_size as f32 * 100.0 value.matched_code as f32 / value.total_code as f32 * 100.0
},
total_data: value.total_data,
matched_data: value.matched_data,
matched_data_percent: if value.total_data == 0 {
100.0
} else {
value.matched_data as f32 / value.total_data as f32 * 100.0
}, },
total_functions: value.total_functions, total_functions: value.total_functions,
matched_functions: value.matched_functions, matched_functions: value.matched_functions,
@ -351,24 +403,25 @@ struct ChangeUnit {
name: String, name: String,
from: Option<ChangeInfo>, from: Option<ChangeInfo>,
to: Option<ChangeInfo>, to: Option<ChangeInfo>,
functions: Vec<ChangeFunction>, sections: Vec<ChangeItem>,
functions: Vec<ChangeItem>,
} }
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
struct ChangeFunction { struct ChangeItem {
name: String, name: String,
from: Option<ChangeFunctionInfo>, from: Option<ChangeItemInfo>,
to: Option<ChangeFunctionInfo>, to: Option<ChangeItemInfo>,
} }
#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
struct ChangeFunctionInfo { struct ChangeItemInfo {
fuzzy_match_percent: f32, fuzzy_match_percent: f32,
size: u64, size: u64,
} }
impl From<&ReportFunction> for ChangeFunctionInfo { impl From<&ReportItem> for ChangeItemInfo {
fn from(value: &ReportFunction) -> Self { fn from(value: &ReportItem) -> Self {
Self { fuzzy_match_percent: value.fuzzy_match_percent, size: value.size } Self { fuzzy_match_percent: value.fuzzy_match_percent, size: value.size }
} }
} }
@ -382,54 +435,18 @@ fn changes(args: ChangesArgs) -> Result<()> {
units: vec![], units: vec![],
}; };
for prev_unit in &previous.units { for prev_unit in &previous.units {
let prev_unit_info = ChangeInfo::from(prev_unit);
let curr_unit = current.units.iter().find(|u| u.name == prev_unit.name); 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_unit_info = ChangeInfo::from(prev_unit);
let curr_unit_info = curr_unit.map(ChangeInfo::from); let curr_unit_info = curr_unit.map(ChangeInfo::from);
let mut functions = vec![];
if let Some(curr_unit) = curr_unit {
for prev_func in &prev_unit.functions {
let prev_func_info = ChangeFunctionInfo::from(prev_func);
let curr_func = curr_unit.functions.iter().find(|f| f.name == prev_func.name);
let curr_func_info = curr_func.map(ChangeFunctionInfo::from);
if let Some(curr_func_info) = curr_func_info {
if prev_func_info != curr_func_info {
functions.push(ChangeFunction {
name: prev_func.name.clone(),
from: Some(prev_func_info),
to: Some(curr_func_info),
});
}
} else {
functions.push(ChangeFunction {
name: prev_func.name.clone(),
from: Some(prev_func_info),
to: None,
});
}
}
for curr_func in &curr_unit.functions {
if !prev_unit.functions.iter().any(|f| f.name == curr_func.name) {
functions.push(ChangeFunction {
name: curr_func.name.clone(),
from: None,
to: Some(ChangeFunctionInfo::from(curr_func)),
});
}
}
} else {
for prev_func in &prev_unit.functions {
functions.push(ChangeFunction {
name: prev_func.name.clone(),
from: Some(ChangeFunctionInfo::from(prev_func)),
to: None,
});
}
}
if !functions.is_empty() || !matches!(&curr_unit_info, Some(v) if v == &prev_unit_info) { if !functions.is_empty() || !matches!(&curr_unit_info, Some(v) if v == &prev_unit_info) {
changes.units.push(ChangeUnit { changes.units.push(ChangeUnit {
name: prev_unit.name.clone(), name: prev_unit.name.clone(),
from: Some(prev_unit_info), from: Some(prev_unit_info),
to: curr_unit_info, to: curr_unit_info,
sections,
functions, functions,
}); });
} }
@ -440,15 +457,8 @@ fn changes(args: ChangesArgs) -> Result<()> {
name: curr_unit.name.clone(), name: curr_unit.name.clone(),
from: None, from: None,
to: Some(ChangeInfo::from(curr_unit)), to: Some(ChangeInfo::from(curr_unit)),
functions: curr_unit sections: process_new_items(&curr_unit.sections),
.functions functions: process_new_items(&curr_unit.functions),
.iter()
.map(|f| ChangeFunction {
name: f.name.clone(),
from: None,
to: Some(ChangeFunctionInfo::from(f)),
})
.collect(),
}); });
} }
} }
@ -466,6 +476,63 @@ fn changes(args: ChangesArgs) -> Result<()> {
Ok(()) 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),
});
}
} else {
items.push(ChangeItem {
name: prev_func.name.clone(),
from: Some(prev_func_info),
to: None,
});
}
}
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)),
});
}
}
} else {
for prev_func in prev_items {
items.push(ChangeItem {
name: prev_func.name.clone(),
from: Some(ChangeItemInfo::from(prev_func)),
to: None,
});
}
}
items
}
fn process_new_items(items: &[ReportItem]) -> Vec<ChangeItem> {
items
.iter()
.map(|f| ChangeItem { name: f.name.clone(), from: None, to: Some(ChangeItemInfo::from(f)) })
.collect()
}
fn read_report(path: &Path) -> Result<Report> { fn read_report(path: &Path) -> Result<Report> {
serde_json::from_reader(BufReader::new( serde_json::from_reader(BufReader::new(
File::open(path).with_context(|| format!("Failed to open {}", path.display()))?, File::open(path).with_context(|| format!("Failed to open {}", path.display()))?,

View File

@ -1,6 +1,6 @@
[package] [package]
name = "objdiff-core" name = "objdiff-core"
version = "1.0.0" version = "2.0.0-alpha.2"
edition = "2021" edition = "2021"
rust-version = "1.70" rust-version = "1.70"
authors = ["Luke Street <luke@street.dev>"] authors = ["Luke Street <luke@street.dev>"]
@ -32,6 +32,7 @@ num-traits = "0.2.18"
object = { version = "0.35.0", features = ["read_core", "std", "elf", "pe"], default-features = false } object = { version = "0.35.0", features = ["read_core", "std", "elf", "pe"], default-features = false }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
similar = { version = "2.5.0", default-features = false } similar = { version = "2.5.0", default-features = false }
strum = { version = "0.26.2", features = ["derive"] }
# config # config
globset = { version = "0.4.14", features = ["serde1"], optional = true } globset = { version = "0.4.14", features = ["serde1"], optional = true }

View File

@ -1,29 +1,56 @@
use std::borrow::Cow; use std::{borrow::Cow, sync::Mutex};
use anyhow::{bail, Result}; use anyhow::{anyhow, bail, Result};
use object::{elf, Endian, Endianness, File, Object, Relocation, RelocationFlags}; use object::{elf, Endian, Endianness, File, FileFlags, Object, Relocation, RelocationFlags};
use rabbitizer::{config, Abi, InstrCategory, Instruction, OperandType}; use rabbitizer::{config, Abi, InstrCategory, Instruction, OperandType};
use crate::{ use crate::{
arch::{ObjArch, ProcessCodeResult}, arch::{ObjArch, ProcessCodeResult},
diff::DiffObjConfig, diff::{DiffObjConfig, MipsAbi, MipsInstrCategory},
obj::{ObjInfo, ObjIns, ObjInsArg, ObjInsArgValue, ObjReloc, ObjSection, SymbolRef}, obj::{ObjInfo, ObjIns, ObjInsArg, ObjInsArgValue, ObjReloc, ObjSection, SymbolRef},
}; };
fn configure_rabbitizer() { static RABBITIZER_MUTEX: Mutex<()> = Mutex::new(());
fn configure_rabbitizer(abi: Abi) {
unsafe { unsafe {
config::RabbitizerConfig_Cfg.reg_names.fpr_abi_names = Abi::O32; config::RabbitizerConfig_Cfg.reg_names.fpr_abi_names = abi;
} }
} }
pub struct ObjArchMips { pub struct ObjArchMips {
pub endianness: Endianness, pub endianness: Endianness,
pub abi: Abi,
pub instr_category: InstrCategory,
} }
const EF_MIPS_ABI: u32 = 0x0000F000;
const EF_MIPS_MACH: u32 = 0x00FF0000;
const E_MIPS_MACH_ALLEGREX: u32 = 0x00840000;
const E_MIPS_MACH_5900: u32 = 0x00920000;
impl ObjArchMips { impl ObjArchMips {
pub fn new(object: &File) -> Result<Self> { pub fn new(object: &File) -> Result<Self> {
configure_rabbitizer(); let mut abi = Abi::NUMERIC;
Ok(Self { endianness: object.endianness() }) 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 => Abi::O32,
elf::EF_MIPS_ABI_EABI32 | elf::EF_MIPS_ABI_EABI64 => Abi::N32,
_ => Abi::NUMERIC,
};
instr_category = match e_flags & EF_MIPS_MACH {
E_MIPS_MACH_ALLEGREX => InstrCategory::R4000ALLEGREX,
E_MIPS_MACH_5900 => InstrCategory::R5900,
_ => InstrCategory::CPU,
};
}
_ => bail!("Unsupported MIPS file flags"),
}
Ok(Self { endianness: object.endianness(), abi, instr_category })
} }
} }
@ -35,9 +62,26 @@ impl ObjArch for ObjArchMips {
config: &DiffObjConfig, config: &DiffObjConfig,
) -> Result<ProcessCodeResult> { ) -> Result<ProcessCodeResult> {
let (section, symbol) = obj.section_symbol(symbol_ref); let (section, symbol) = obj.section_symbol(symbol_ref);
let section = section.ok_or_else(|| anyhow!("Code symbol section not found"))?;
let code = &section.data let code = &section.data
[symbol.section_address as usize..(symbol.section_address + symbol.size) as usize]; [symbol.section_address as usize..(symbol.section_address + symbol.size) as usize];
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 = symbol.address; let start_address = symbol.address;
let end_address = symbol.address + symbol.size; let end_address = symbol.address + symbol.size;
let ins_count = code.len() / 4; let ins_count = code.len() / 4;
@ -47,7 +91,7 @@ impl ObjArch for ObjArchMips {
for chunk in code.chunks_exact(4) { for chunk in code.chunks_exact(4) {
let reloc = section.relocations.iter().find(|r| (r.address as u32 & !3) == cur_addr); let reloc = section.relocations.iter().find(|r| (r.address as u32 & !3) == cur_addr);
let code = self.endianness.read_u32_bytes(chunk.try_into()?); let code = self.endianness.read_u32_bytes(chunk.try_into()?);
let instruction = Instruction::new(code, cur_addr, InstrCategory::CPU); let instruction = Instruction::new(code, cur_addr, instr_category);
let formatted = instruction.disassemble(None, 0); let formatted = instruction.disassemble(None, 0);
let op = instruction.unique_id as u16; let op = instruction.unique_id as u16;

View File

@ -1,6 +1,6 @@
use std::borrow::Cow; use std::borrow::Cow;
use anyhow::{bail, Result}; use anyhow::{anyhow, bail, Result};
use object::{elf, File, Relocation, RelocationFlags}; use object::{elf, File, Relocation, RelocationFlags};
use ppc750cl::{Argument, InsIter, GPR}; use ppc750cl::{Argument, InsIter, GPR};
@ -36,6 +36,7 @@ impl ObjArch for ObjArchPpc {
config: &DiffObjConfig, config: &DiffObjConfig,
) -> Result<ProcessCodeResult> { ) -> Result<ProcessCodeResult> {
let (section, symbol) = obj.section_symbol(symbol_ref); let (section, symbol) = obj.section_symbol(symbol_ref);
let section = section.ok_or_else(|| anyhow!("Code symbol section not found"))?;
let code = &section.data let code = &section.data
[symbol.section_address as usize..(symbol.section_address + symbol.size) as usize]; [symbol.section_address as usize..(symbol.section_address + symbol.size) as usize];

View File

@ -33,6 +33,7 @@ impl ObjArch for ObjArchX86 {
config: &DiffObjConfig, config: &DiffObjConfig,
) -> Result<ProcessCodeResult> { ) -> Result<ProcessCodeResult> {
let (section, symbol) = obj.section_symbol(symbol_ref); let (section, symbol) = obj.section_symbol(symbol_ref);
let section = section.ok_or_else(|| anyhow!("Code symbol section not found"))?;
let code = &section.data let code = &section.data
[symbol.section_address as usize..(symbol.section_address + symbol.size) as usize]; [symbol.section_address as usize..(symbol.section_address + symbol.size) as usize];
@ -264,7 +265,7 @@ impl FormatterOutput for InstructionFormatterOutput {
FormatterTextKind::LabelAddress => { FormatterTextKind::LabelAddress => {
if let Some(reloc) = self.ins.reloc.as_ref() { if let Some(reloc) = self.ins.reloc.as_ref() {
if matches!(reloc.flags, RelocationFlags::Coff { if matches!(reloc.flags, RelocationFlags::Coff {
typ: pe::IMAGE_REL_I386_DIR32 typ: pe::IMAGE_REL_I386_DIR32 | pe::IMAGE_REL_I386_REL32
}) { }) {
self.ins.args.push(ObjInsArg::Reloc); self.ins.args.push(ObjInsArg::Reloc);
return; return;

View File

@ -143,10 +143,11 @@ fn validate_min_version(config: &ProjectConfig) -> Result<()> {
let Some(min_version) = &config.min_version else { return Ok(()) }; let Some(min_version) = &config.min_version else { return Ok(()) };
let version = semver::Version::parse(env!("CARGO_PKG_VERSION")) let version = semver::Version::parse(env!("CARGO_PKG_VERSION"))
.context("Failed to parse package version")?; .context("Failed to parse package version")?;
match semver::VersionReq::parse(&format!(">={min_version}")) { let min_version = semver::Version::parse(min_version).context("Failed to parse min_version")?;
Ok(version_req) if version_req.matches(&version) => Ok(()), if version >= min_version {
Ok(_) => Err(anyhow!("Project requires objdiff version {min_version} or higher")), Ok(())
Err(e) => Err(anyhow::Error::new(e).context("Failed to parse min_version")), } else {
Err(anyhow!("Project requires objdiff version {min_version} or higher"))
} }
} }

View File

@ -28,7 +28,7 @@ pub fn no_diff_code(
diff.push(ObjInsDiff { ins: Some(i), kind: ObjInsDiffKind::None, ..Default::default() }); diff.push(ObjInsDiff { ins: Some(i), kind: ObjInsDiffKind::None, ..Default::default() });
} }
resolve_branches(&mut diff); resolve_branches(&mut diff);
Ok(ObjSymbolDiff { diff_symbol: None, instructions: diff, match_percent: None }) Ok(ObjSymbolDiff { symbol_ref, diff_symbol: None, instructions: diff, match_percent: None })
} }
pub fn diff_code( pub fn diff_code(
@ -66,11 +66,13 @@ pub fn diff_code(
Ok(( Ok((
ObjSymbolDiff { ObjSymbolDiff {
symbol_ref: left_symbol_ref,
diff_symbol: Some(right_symbol_ref), diff_symbol: Some(right_symbol_ref),
instructions: left_diff, instructions: left_diff,
match_percent: Some(percent), match_percent: Some(percent),
}, },
ObjSymbolDiff { ObjSymbolDiff {
symbol_ref: right_symbol_ref,
diff_symbol: Some(left_symbol_ref), diff_symbol: Some(left_symbol_ref),
instructions: right_diff, instructions: right_diff,
match_percent: Some(percent), match_percent: Some(percent),

View File

@ -3,14 +3,35 @@ use std::{
time::{Duration, Instant}, time::{Duration, Instant},
}; };
use anyhow::Result; use anyhow::{anyhow, Result};
use similar::{capture_diff_slices_deadline, Algorithm}; use similar::{capture_diff_slices_deadline, get_diff_ratio, Algorithm};
use crate::{ use crate::{
diff::{ObjDataDiff, ObjDataDiffKind, ObjSectionDiff, ObjSymbolDiff}, diff::{ObjDataDiff, ObjDataDiffKind, ObjSectionDiff, ObjSymbolDiff},
obj::{ObjInfo, ObjSection, SymbolRef}, obj::{ObjInfo, ObjSection, SymbolRef},
}; };
/// Compare the addresses and sizes of each symbol in the BSS sections.
pub fn diff_bss_section(
left: &ObjSection,
right: &ObjSection,
) -> Result<(ObjSectionDiff, ObjSectionDiff)> {
let deadline = Instant::now() + Duration::from_secs(5);
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,
Some(deadline),
);
let match_percent = get_diff_ratio(&ops, left_sizes.len(), right_sizes.len()) * 100.0;
Ok((
ObjSectionDiff { symbols: vec![], data_diff: vec![], match_percent: Some(match_percent) },
ObjSectionDiff { symbols: vec![], data_diff: vec![], match_percent: Some(match_percent) },
))
}
pub fn diff_bss_symbol( pub fn diff_bss_symbol(
left_obj: &ObjInfo, left_obj: &ObjInfo,
right_obj: &ObjInfo, right_obj: &ObjInfo,
@ -22,11 +43,13 @@ pub fn diff_bss_symbol(
let percent = if left_symbol.size == right_symbol.size { 100.0 } else { 50.0 }; let percent = if left_symbol.size == right_symbol.size { 100.0 } else { 50.0 };
Ok(( Ok((
ObjSymbolDiff { ObjSymbolDiff {
symbol_ref: left_symbol_ref,
diff_symbol: Some(right_symbol_ref), diff_symbol: Some(right_symbol_ref),
instructions: vec![], instructions: vec![],
match_percent: Some(percent), match_percent: Some(percent),
}, },
ObjSymbolDiff { ObjSymbolDiff {
symbol_ref: right_symbol_ref,
diff_symbol: Some(left_symbol_ref), diff_symbol: Some(left_symbol_ref),
instructions: vec![], instructions: vec![],
match_percent: Some(percent), match_percent: Some(percent),
@ -34,17 +57,23 @@ pub fn diff_bss_symbol(
)) ))
} }
pub fn no_diff_bss_symbol(_obj: &ObjInfo, _symbol_ref: SymbolRef) -> ObjSymbolDiff { pub fn no_diff_symbol(_obj: &ObjInfo, symbol_ref: SymbolRef) -> ObjSymbolDiff {
ObjSymbolDiff { diff_symbol: None, instructions: vec![], match_percent: Some(0.0) } ObjSymbolDiff { symbol_ref, diff_symbol: None, instructions: vec![], match_percent: None }
} }
pub fn diff_data( /// Compare the data sections of two object files.
pub fn diff_data_section(
left: &ObjSection, left: &ObjSection,
right: &ObjSection, right: &ObjSection,
) -> Result<(ObjSectionDiff, ObjSectionDiff)> { ) -> Result<(ObjSectionDiff, ObjSectionDiff)> {
let deadline = Instant::now() + Duration::from_secs(5); let deadline = Instant::now() + Duration::from_secs(5);
let left_max = left.symbols.iter().map(|s| s.section_address + s.size).max().unwrap_or(0);
let right_max = right.symbols.iter().map(|s| s.section_address + s.size).max().unwrap_or(0);
let left_data = &left.data[..left_max as usize];
let right_data = &right.data[..right_max as usize];
let ops = let ops =
capture_diff_slices_deadline(Algorithm::Patience, &left.data, &right.data, Some(deadline)); capture_diff_slices_deadline(Algorithm::Patience, left_data, right_data, Some(deadline));
let match_percent = get_diff_ratio(&ops, left_data.len(), right_data.len()) * 100.0;
let mut left_diff = Vec::<ObjDataDiff>::new(); let mut left_diff = Vec::<ObjDataDiff>::new();
let mut right_diff = Vec::<ObjDataDiff>::new(); let mut right_diff = Vec::<ObjDataDiff>::new();
@ -118,14 +147,71 @@ pub fn diff_data(
ObjSectionDiff { ObjSectionDiff {
symbols: vec![], symbols: vec![],
data_diff: left_diff, data_diff: left_diff,
// TODO match_percent: Some(match_percent),
match_percent: None,
}, },
ObjSectionDiff { ObjSectionDiff {
symbols: vec![], symbols: vec![],
data_diff: right_diff, data_diff: right_diff,
// TODO match_percent: Some(match_percent),
match_percent: None,
}, },
)) ))
} }
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 deadline = Instant::now() + Duration::from_secs(5);
let ops =
capture_diff_slices_deadline(Algorithm::Patience, left_data, right_data, Some(deadline));
let match_percent = get_diff_ratio(&ops, left_data.len(), right_data.len()) * 100.0;
Ok((
ObjSymbolDiff {
symbol_ref: left_symbol_ref,
diff_symbol: Some(right_symbol_ref),
instructions: vec![],
match_percent: Some(match_percent),
},
ObjSymbolDiff {
symbol_ref: right_symbol_ref,
diff_symbol: Some(left_symbol_ref),
instructions: vec![],
match_percent: Some(match_percent),
},
))
}
/// Compare the text sections of two object files.
/// This essentially adds up the match percentage of each symbol in the text section.
pub fn diff_text_section(
left: &ObjSection,
_right: &ObjSection,
left_diff: &ObjSectionDiff,
_right_diff: &ObjSectionDiff,
) -> Result<(ObjSectionDiff, ObjSectionDiff)> {
let match_percent = 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) },
))
}

View File

@ -1,7 +1,3 @@
mod code;
mod data;
pub mod display;
use std::collections::HashSet; use std::collections::HashSet;
use anyhow::Result; use anyhow::Result;
@ -9,30 +5,120 @@ use anyhow::Result;
use crate::{ use crate::{
diff::{ diff::{
code::{diff_code, no_diff_code}, code::{diff_code, no_diff_code},
data::{diff_bss_symbol, diff_data, no_diff_bss_symbol}, data::{
diff_bss_section, diff_bss_symbol, diff_data_section, diff_data_symbol,
diff_text_section, no_diff_symbol,
}, },
obj::{ObjInfo, ObjIns, ObjSectionKind, SymbolRef}, },
obj::{ObjInfo, ObjIns, ObjSection, ObjSectionKind, ObjSymbol, SymbolRef},
}; };
#[derive(Debug, Copy, Clone, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)] mod code;
mod data;
pub mod display;
#[derive(
Debug,
Copy,
Clone,
Default,
Eq,
PartialEq,
serde::Deserialize,
serde::Serialize,
strum::VariantArray,
strum::EnumMessage,
)]
pub enum X86Formatter { pub enum X86Formatter {
#[default] #[default]
#[strum(message = "Intel (default)")]
Intel, Intel,
#[strum(message = "AT&T")]
Gas, Gas,
#[strum(message = "NASM")]
Nasm, Nasm,
#[strum(message = "MASM")]
Masm, Masm,
} }
#[derive(
Debug,
Copy,
Clone,
Default,
Eq,
PartialEq,
serde::Deserialize,
serde::Serialize,
strum::VariantArray,
strum::EnumMessage,
)]
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,
)]
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,
}
#[inline] #[inline]
const fn default_true() -> bool { true } const fn default_true() -> bool { true }
#[derive(Debug, Clone, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)] #[derive(Debug, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(default)] #[serde(default)]
pub struct DiffObjConfig { pub struct DiffObjConfig {
pub relax_reloc_diffs: bool, pub relax_reloc_diffs: bool,
#[serde(default = "default_true")] #[serde(default = "default_true")]
pub space_between_args: bool, pub space_between_args: bool,
// x86
pub x86_formatter: X86Formatter, pub x86_formatter: X86Formatter,
// MIPS
pub mips_abi: MipsAbi,
pub mips_instr_category: MipsInstrCategory,
}
impl Default for DiffObjConfig {
fn default() -> Self {
Self {
relax_reloc_diffs: false,
space_between_args: true,
x86_formatter: Default::default(),
mips_abi: Default::default(),
mips_instr_category: Default::default(),
}
}
} }
impl DiffObjConfig { impl DiffObjConfig {
@ -62,6 +148,7 @@ impl ObjSectionDiff {
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct ObjSymbolDiff { pub struct ObjSymbolDiff {
pub symbol_ref: SymbolRef,
pub diff_symbol: Option<SymbolRef>, pub diff_symbol: Option<SymbolRef>,
pub instructions: Vec<ObjInsDiff>, pub instructions: Vec<ObjInsDiff>,
pub match_percent: Option<f32>, pub match_percent: Option<f32>,
@ -142,10 +229,11 @@ impl ObjDiff {
sections: Vec::with_capacity(obj.sections.len()), sections: Vec::with_capacity(obj.sections.len()),
common: Vec::with_capacity(obj.common.len()), common: Vec::with_capacity(obj.common.len()),
}; };
for section in &obj.sections { for (section_idx, section) in obj.sections.iter().enumerate() {
let mut symbols = Vec::with_capacity(section.symbols.len()); let mut symbols = Vec::with_capacity(section.symbols.len());
for _ in &section.symbols { for (symbol_idx, _) in section.symbols.iter().enumerate() {
symbols.push(ObjSymbolDiff { symbols.push(ObjSymbolDiff {
symbol_ref: SymbolRef { section_idx, symbol_idx },
diff_symbol: None, diff_symbol: None,
instructions: vec![], instructions: vec![],
match_percent: None, match_percent: None,
@ -162,8 +250,9 @@ impl ObjDiff {
match_percent: None, match_percent: None,
}); });
} }
for _ in &obj.common { for (symbol_idx, _) in obj.common.iter().enumerate() {
result.common.push(ObjSymbolDiff { result.common.push(ObjSymbolDiff {
symbol_ref: SymbolRef { section_idx: obj.sections.len(), symbol_idx },
diff_symbol: None, diff_symbol: None,
instructions: vec![], instructions: vec![],
match_percent: None, match_percent: None,
@ -184,13 +273,21 @@ impl ObjDiff {
#[inline] #[inline]
pub fn symbol_diff(&self, symbol_ref: SymbolRef) -> &ObjSymbolDiff { pub fn symbol_diff(&self, symbol_ref: SymbolRef) -> &ObjSymbolDiff {
if symbol_ref.section_idx == self.sections.len() {
&self.common[symbol_ref.symbol_idx]
} else {
&self.section_diff(symbol_ref.section_idx).symbols[symbol_ref.symbol_idx] &self.section_diff(symbol_ref.section_idx).symbols[symbol_ref.symbol_idx]
} }
}
#[inline] #[inline]
pub fn symbol_diff_mut(&mut self, symbol_ref: SymbolRef) -> &mut ObjSymbolDiff { pub fn symbol_diff_mut(&mut self, symbol_ref: SymbolRef) -> &mut ObjSymbolDiff {
if symbol_ref.section_idx == self.sections.len() {
&mut self.common[symbol_ref.symbol_idx]
} else {
&mut self.section_diff_mut(symbol_ref.section_idx).symbols[symbol_ref.symbol_idx] &mut self.section_diff_mut(symbol_ref.section_idx).symbols[symbol_ref.symbol_idx]
} }
}
} }
#[derive(Default)] #[derive(Default)]
@ -247,7 +344,14 @@ pub fn diff_objs(
} }
} }
ObjSectionKind::Data => { ObjSectionKind::Data => {
// TODO diff data symbol 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 => { ObjSectionKind::Bss => {
let (left_diff, right_diff) = diff_bss_symbol( let (left_diff, right_diff) = diff_bss_symbol(
@ -268,10 +372,9 @@ pub fn diff_objs(
*left_out.symbol_diff_mut(left_symbol_ref) = *left_out.symbol_diff_mut(left_symbol_ref) =
no_diff_code(left_obj, left_symbol_ref, config)?; no_diff_code(left_obj, left_symbol_ref, config)?;
} }
ObjSectionKind::Data => {} ObjSectionKind::Data | ObjSectionKind::Bss => {
ObjSectionKind::Bss => {
*left_out.symbol_diff_mut(left_symbol_ref) = *left_out.symbol_diff_mut(left_symbol_ref) =
no_diff_bss_symbol(left_obj, left_symbol_ref); no_diff_symbol(left_obj, left_symbol_ref);
} }
} }
} }
@ -282,10 +385,9 @@ pub fn diff_objs(
*right_out.symbol_diff_mut(right_symbol_ref) = *right_out.symbol_diff_mut(right_symbol_ref) =
no_diff_code(right_obj, right_symbol_ref, config)?; no_diff_code(right_obj, right_symbol_ref, config)?;
} }
ObjSectionKind::Data => {} ObjSectionKind::Data | ObjSectionKind::Bss => {
ObjSectionKind::Bss => {
*right_out.symbol_diff_mut(right_symbol_ref) = *right_out.symbol_diff_mut(right_symbol_ref) =
no_diff_bss_symbol(right_obj, right_symbol_ref); no_diff_symbol(right_obj, right_symbol_ref);
} }
} }
} }
@ -308,15 +410,26 @@ pub fn diff_objs(
let right_section = &right_obj.sections[right_section_idx]; let right_section = &right_obj.sections[right_section_idx];
match section_kind { match section_kind {
ObjSectionKind::Code => { ObjSectionKind::Code => {
// TODO? 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_text_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 => { ObjSectionKind::Data => {
let (left_diff, right_diff) = diff_data(left_section, right_section)?; let (left_diff, right_diff) = diff_data_section(left_section, right_section)?;
left_out.section_diff_mut(left_section_idx).merge(left_diff); left_out.section_diff_mut(left_section_idx).merge(left_diff);
right_out.section_diff_mut(right_section_idx).merge(right_diff); right_out.section_diff_mut(right_section_idx).merge(right_diff);
} }
ObjSectionKind::Bss => { ObjSectionKind::Bss => {
// TODO let (left_diff, right_diff) = diff_bss_section(left_section, right_section)?;
left_out.section_diff_mut(left_section_idx).merge(left_diff);
right_out.section_diff_mut(right_section_idx).merge(right_diff);
} }
} }
} }
@ -357,8 +470,8 @@ fn matching_symbols(
for (symbol_idx, symbol) in section.symbols.iter().enumerate() { for (symbol_idx, symbol) in section.symbols.iter().enumerate() {
let symbol_match = SymbolMatch { let symbol_match = SymbolMatch {
left: Some(SymbolRef { section_idx, symbol_idx }), left: Some(SymbolRef { section_idx, symbol_idx }),
right: find_symbol(right, &symbol.name, section.kind), right: find_symbol(right, symbol, section),
prev: find_symbol(prev, &symbol.name, section.kind), prev: find_symbol(prev, symbol, section),
section_kind: section.kind, section_kind: section.kind,
}; };
matches.push(symbol_match); matches.push(symbol_match);
@ -367,6 +480,18 @@ fn matching_symbols(
} }
} }
} }
for (symbol_idx, symbol) in left.common.iter().enumerate() {
let symbol_match = SymbolMatch {
left: Some(SymbolRef { section_idx: left.sections.len(), symbol_idx }),
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 { if let Some(right) = right {
for (section_idx, section) in right.sections.iter().enumerate() { for (section_idx, section) in right.sections.iter().enumerate() {
@ -378,30 +503,69 @@ fn matching_symbols(
matches.push(SymbolMatch { matches.push(SymbolMatch {
left: None, left: None,
right: Some(symbol_ref), right: Some(symbol_ref),
prev: find_symbol(prev, &symbol.name, section.kind), prev: find_symbol(prev, symbol, section),
section_kind: section.kind, section_kind: section.kind,
}); });
} }
} }
for (symbol_idx, symbol) in right.common.iter().enumerate() {
let symbol_ref = SymbolRef { section_idx: right.sections.len(), 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) Ok(matches)
} }
fn find_symbol( fn find_symbol(
obj: Option<&ObjInfo>, obj: Option<&ObjInfo>,
name: &str, in_symbol: &ObjSymbol,
section_kind: ObjSectionKind, in_section: &ObjSection,
) -> Option<SymbolRef> { ) -> Option<SymbolRef> {
for (section_idx, section) in obj?.sections.iter().enumerate() { let obj = obj?;
if section.kind != section_kind { // Try to find an exact name match
for (section_idx, section) in obj.sections.iter().enumerate() {
if section.kind != in_section.kind {
continue; continue;
} }
let symbol_idx = match section.symbols.iter().position(|symbol| symbol.name == name) { if let Some(symbol_idx) =
Some(symbol_idx) => symbol_idx, section.symbols.iter().position(|symbol| symbol.name == in_symbol.name)
None => continue, {
};
return Some(SymbolRef { section_idx, symbol_idx }); 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) = section.symbols.iter().position(|symbol| {
symbol.address == in_symbol.address && symbol.name.starts_with('@')
}) {
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: obj.sections.len(), symbol_idx });
}
}
None None
} }

View File

@ -142,16 +142,20 @@ pub struct ObjReloc {
pub target_section: Option<String>, pub target_section: Option<String>,
} }
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct SymbolRef { pub struct SymbolRef {
pub section_idx: usize, pub section_idx: usize,
pub symbol_idx: usize, pub symbol_idx: usize,
} }
impl ObjInfo { impl ObjInfo {
pub fn section_symbol(&self, symbol_ref: SymbolRef) -> (&ObjSection, &ObjSymbol) { pub fn section_symbol(&self, symbol_ref: SymbolRef) -> (Option<&ObjSection>, &ObjSymbol) {
if symbol_ref.section_idx == self.sections.len() {
let symbol = &self.common[symbol_ref.symbol_idx];
return (None, symbol);
}
let section = &self.sections[symbol_ref.section_idx]; let section = &self.sections[symbol_ref.section_idx];
let symbol = &section.symbols[symbol_ref.symbol_idx]; let symbol = &section.symbols[symbol_ref.symbol_idx];
(section, symbol) (Some(section), symbol)
} }
} }

View File

@ -346,15 +346,14 @@ fn line_info(obj_file: &File<'_>, sections: &mut [ObjSection]) -> Result<()> {
if row.end_sequence() { if row.end_sequence() {
// The next row is the start of a new sequence, which means we must // The next row is the start of a new sequence, which means we must
// advance to the next .text section. // advance to the next .text section.
let section_index = text_sections let section_index = text_sections.next().map(|s| s.index().0);
.next() lines = section_index.map(|index| {
.ok_or_else(|| anyhow!("Next text section not found for line info"))? &mut sections
.index()
.0;
lines = sections
.iter_mut() .iter_mut()
.find(|s| s.orig_index == section_index) .find(|s| s.orig_index == index)
.map(|s| &mut s.line_info); .unwrap()
.line_info
});
} }
} }
} }

View File

@ -1,6 +1,6 @@
[package] [package]
name = "objdiff-gui" name = "objdiff-gui"
version = "1.0.0" version = "2.0.0-alpha.2"
edition = "2021" edition = "2021"
rust-version = "1.70" rust-version = "1.70"
authors = ["Luke Street <luke@street.dev>"] authors = ["Luke Street <luke@street.dev>"]
@ -46,6 +46,7 @@ ron = "0.8.1"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1.0.116" serde_json = "1.0.116"
shell-escape = "0.1.5" shell-escape = "0.1.5"
strum = { version = "0.26.2", features = ["derive"] }
tempfile = "3.10.1" tempfile = "3.10.1"
time = { version = "0.3.36", features = ["formatting", "local-offset"] } time = { version = "0.3.36", features = ["formatting", "local-offset"] }

View File

@ -30,7 +30,7 @@ use crate::{
views::{ views::{
appearance::{appearance_window, Appearance}, appearance::{appearance_window, Appearance},
config::{ config::{
config_ui, diff_config_window, project_window, ConfigViewState, CONFIG_DISABLED_TEXT, arch_config_window, config_ui, project_window, ConfigViewState, CONFIG_DISABLED_TEXT,
}, },
data_diff::data_diff_ui, data_diff::data_diff_ui,
debug::debug_window, debug::debug_window,
@ -52,7 +52,7 @@ pub struct ViewState {
pub show_appearance_config: bool, pub show_appearance_config: bool,
pub show_demangle: bool, pub show_demangle: bool,
pub show_project_config: bool, pub show_project_config: bool,
pub show_diff_config: bool, pub show_arch_config: bool,
pub show_debug: bool, pub show_debug: bool,
} }
@ -414,7 +414,7 @@ impl eframe::App for App {
show_appearance_config, show_appearance_config,
show_demangle, show_demangle,
show_project_config, show_project_config,
show_diff_config, show_arch_config,
show_debug, show_debug,
} = view_state; } = view_state;
@ -468,8 +468,8 @@ impl eframe::App for App {
} }
}); });
ui.menu_button("Diff Options", |ui| { ui.menu_button("Diff Options", |ui| {
if ui.button("More").clicked() { if ui.button("Arch Settings").clicked() {
*show_diff_config = !*show_diff_config; *show_arch_config = !*show_arch_config;
ui.close_menu(); ui.close_menu();
} }
let mut config = config.write().unwrap(); let mut config = config.write().unwrap();
@ -541,7 +541,7 @@ impl eframe::App for App {
project_window(ctx, config, show_project_config, config_state, appearance); project_window(ctx, config, show_project_config, config_state, appearance);
appearance_window(ctx, show_appearance_config, appearance); appearance_window(ctx, show_appearance_config, appearance);
demangle_window(ctx, show_demangle, demangle_state, appearance); demangle_window(ctx, show_demangle, demangle_state, appearance);
diff_config_window(ctx, config, show_diff_config, appearance); arch_config_window(ctx, config, show_arch_config, appearance);
debug_window(ctx, show_debug, frame_history, appearance); debug_window(ctx, show_debug, frame_history, appearance);
self.post_update(ctx); self.post_update(ctx);

View File

@ -5,13 +5,13 @@ use self_update::{cargo_crate_version, update::Release};
use crate::{ use crate::{
jobs::{start_job, update_status, Job, JobContext, JobResult, JobState}, jobs::{start_job, update_status, Job, JobContext, JobResult, JobState},
update::{build_updater, BIN_NAME}, update::{build_updater, BIN_NAME_NEW, BIN_NAME_OLD},
}; };
pub struct CheckUpdateResult { pub struct CheckUpdateResult {
pub update_available: bool, pub update_available: bool,
pub latest_release: Release, pub latest_release: Release,
pub found_binary: bool, pub found_binary: Option<String>,
} }
fn run_check_update(context: &JobContext, cancel: Receiver<()>) -> Result<Box<CheckUpdateResult>> { fn run_check_update(context: &JobContext, cancel: Receiver<()>) -> Result<Box<CheckUpdateResult>> {
@ -20,7 +20,13 @@ fn run_check_update(context: &JobContext, cancel: Receiver<()>) -> Result<Box<Ch
let latest_release = updater.get_latest_release()?; let latest_release = updater.get_latest_release()?;
let update_available = let update_available =
self_update::version::bump_is_greater(cargo_crate_version!(), &latest_release.version)?; 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); // 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)?; update_status(context, "Complete".to_string(), 1, 1, &cancel)?;
Ok(Box::new(CheckUpdateResult { update_available, latest_release, found_binary })) Ok(Box::new(CheckUpdateResult { update_available, latest_release, found_binary }))

View File

@ -6,26 +6,29 @@ use std::{
}; };
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use const_format::formatcp;
use crate::{ use crate::{
jobs::{start_job, update_status, Job, JobContext, JobResult, JobState}, jobs::{start_job, update_status, Job, JobContext, JobResult, JobState},
update::{build_updater, BIN_NAME}, update::build_updater,
}; };
pub struct UpdateResult { pub struct UpdateResult {
pub exe_path: PathBuf, pub exe_path: PathBuf,
} }
fn run_update(status: &JobContext, 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)?; update_status(status, "Fetching latest release".to_string(), 0, 3, &cancel)?;
let updater = build_updater().context("Failed to create release updater")?; let updater = build_updater().context("Failed to create release updater")?;
let latest_release = updater.get_latest_release()?; let latest_release = updater.get_latest_release()?;
let asset = latest_release let asset = latest_release
.assets .assets
.iter() .iter()
.find(|a| a.name == BIN_NAME) .find(|a| a.name == bin_name)
.ok_or_else(|| anyhow::Error::msg(formatcp!("No release asset for {}", 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)?; update_status(status, "Downloading release".to_string(), 1, 3, &cancel)?;
let tmp_dir = tempfile::Builder::new().prefix("update").tempdir_in(current_dir()?)?; let tmp_dir = tempfile::Builder::new().prefix("update").tempdir_in(current_dir()?)?;
@ -53,8 +56,8 @@ fn run_update(status: &JobContext, cancel: Receiver<()>) -> Result<Box<UpdateRes
Ok(Box::from(UpdateResult { exe_path: target_file })) Ok(Box::from(UpdateResult { exe_path: target_file }))
} }
pub fn start_update(ctx: &egui::Context) -> JobState { pub fn start_update(ctx: &egui::Context, bin_name: String) -> JobState {
start_job(ctx, "Update app", Job::Update, move |context, cancel| { start_job(ctx, "Update app", Job::Update, move |context, cancel| {
run_update(&context, cancel).map(JobResult::Update) run_update(&context, cancel, bin_name).map(JobResult::Update)
}) })
} }

View File

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

View File

@ -16,9 +16,10 @@ use egui::{
use globset::Glob; use globset::Glob;
use objdiff_core::{ use objdiff_core::{
config::{ProjectObject, DEFAULT_WATCH_PATTERNS}, config::{ProjectObject, DEFAULT_WATCH_PATTERNS},
diff::X86Formatter, diff::{MipsAbi, MipsInstrCategory, X86Formatter},
}; };
use self_update::cargo_crate_version; use self_update::cargo_crate_version;
use strum::{EnumMessage, VariantArray};
use crate::{ use crate::{
app::{AppConfig, AppConfigRef, ObjectConfig}, app::{AppConfig, AppConfigRef, ObjectConfig},
@ -41,7 +42,7 @@ pub struct ConfigViewState {
pub check_update_running: bool, pub check_update_running: bool,
pub queue_check_update: bool, pub queue_check_update: bool,
pub update_running: bool, pub update_running: bool,
pub queue_update: bool, pub queue_update: Option<String>,
pub build_running: bool, pub build_running: bool,
pub queue_build: bool, pub queue_build: bool,
pub watch_pattern_text: String, pub watch_pattern_text: String,
@ -127,9 +128,8 @@ impl ConfigViewState {
jobs.push_once(Job::CheckUpdate, || start_check_update(ctx)); jobs.push_once(Job::CheckUpdate, || start_check_update(ctx));
} }
if self.queue_update { if let Some(bin_name) = self.queue_update.take() {
self.queue_update = false; jobs.push_once(Job::Update, || start_update(ctx, bin_name));
jobs.push_once(Job::Update, || start_update(ctx));
} }
} }
} }
@ -201,15 +201,16 @@ pub fn config_ui(
if result.update_available { if result.update_available {
ui.colored_label(appearance.insert_color, "Update available"); ui.colored_label(appearance.insert_color, "Update available");
ui.horizontal(|ui| { ui.horizontal(|ui| {
if result.found_binary if let Some(bin_name) = &result.found_binary {
&& ui if ui
.add_enabled(!state.update_running, egui::Button::new("Automatic")) .add_enabled(!state.update_running, egui::Button::new("Automatic"))
.on_hover_text_at_pointer( .on_hover_text_at_pointer(
"Automatically download and replace the current build", "Automatically download and replace the current build",
) )
.clicked() .clicked()
{ {
state.queue_update = true; state.queue_update = Some(bin_name.clone());
}
} }
if ui if ui
.button("Manual") .button("Manual")
@ -242,7 +243,7 @@ pub fn config_ui(
Box::pin( Box::pin(
rfd::AsyncFileDialog::new() rfd::AsyncFileDialog::new()
.set_directory(&target_dir) .set_directory(&target_dir)
.add_filter("Object file", &["o", "elf"]) .add_filter("Object file", &["o", "elf", "obj"])
.pick_file(), .pick_file(),
) )
}, },
@ -842,29 +843,28 @@ fn split_obj_config_ui(
}); });
} }
pub fn diff_config_window( pub fn arch_config_window(
ctx: &egui::Context, ctx: &egui::Context,
config: &AppConfigRef, config: &AppConfigRef,
show: &mut bool, show: &mut bool,
appearance: &Appearance, appearance: &Appearance,
) { ) {
let mut config_guard = config.write().unwrap(); let mut config_guard = config.write().unwrap();
egui::Window::new("Diff Config").open(show).show(ctx, |ui| { egui::Window::new("Arch Settings").open(show).show(ctx, |ui| {
diff_config_ui(ui, &mut config_guard, appearance); arch_config_ui(ui, &mut config_guard, appearance);
}); });
} }
fn diff_config_ui(ui: &mut egui::Ui, config: &mut AppConfig, _appearance: &Appearance) { fn arch_config_ui(ui: &mut egui::Ui, config: &mut AppConfig, _appearance: &Appearance) {
egui::ComboBox::new("x86_formatter", "X86 Format") ui.heading("x86");
.selected_text(format!("{:?}", config.diff_obj_config.x86_formatter)) egui::ComboBox::new("x86_formatter", "Format")
.selected_text(config.diff_obj_config.x86_formatter.get_message().unwrap())
.show_ui(ui, |ui| { .show_ui(ui, |ui| {
for &formatter in for &formatter in X86Formatter::VARIANTS {
&[X86Formatter::Intel, X86Formatter::Gas, X86Formatter::Nasm, X86Formatter::Masm]
{
if ui if ui
.selectable_label( .selectable_label(
config.diff_obj_config.x86_formatter == formatter, config.diff_obj_config.x86_formatter == formatter,
format!("{:?}", formatter), formatter.get_message().unwrap(),
) )
.clicked() .clicked()
{ {
@ -873,4 +873,38 @@ fn diff_config_ui(ui: &mut egui::Ui, config: &mut AppConfig, _appearance: &Appea
} }
} }
}); });
ui.separator();
ui.heading("MIPS");
egui::ComboBox::new("mips_abi", "ABI")
.selected_text(config.diff_obj_config.mips_abi.get_message().unwrap())
.show_ui(ui, |ui| {
for &abi in MipsAbi::VARIANTS {
if ui
.selectable_label(
config.diff_obj_config.mips_abi == abi,
abi.get_message().unwrap(),
)
.clicked()
{
config.diff_obj_config.mips_abi = abi;
config.queue_reload = true;
}
}
});
egui::ComboBox::new("mips_instr_category", "Instruction Category")
.selected_text(config.diff_obj_config.mips_instr_category.get_message().unwrap())
.show_ui(ui, |ui| {
for &category in MipsInstrCategory::VARIANTS {
if ui
.selectable_label(
config.diff_obj_config.mips_instr_category == category,
category.get_message().unwrap(),
)
.clicked()
{
config.diff_obj_config.mips_instr_category = category;
config.queue_reload = true;
}
}
});
} }

View File

@ -282,6 +282,7 @@ fn asm_col_ui(
ins_view_state: &mut FunctionViewState, ins_view_state: &mut FunctionViewState,
) { ) {
let (section, symbol) = obj.0.section_symbol(symbol_ref); let (section, symbol) = obj.0.section_symbol(symbol_ref);
let section = section.unwrap();
let ins_diff = &obj.1.symbol_diff(symbol_ref).instructions[row.index()]; let ins_diff = &obj.1.symbol_diff(symbol_ref).instructions[row.index()];
let response_cb = |response: Response| { let response_cb = |response: Response| {
if let Some(ins) = &ins_diff.ins { if let Some(ins) = &ins_diff.ins {

View File

@ -7,7 +7,7 @@ use egui::{
use egui_extras::{Size, StripBuilder}; use egui_extras::{Size, StripBuilder};
use objdiff_core::{ use objdiff_core::{
diff::{ObjDiff, ObjSymbolDiff}, diff::{ObjDiff, ObjSymbolDiff},
obj::{ObjInfo, ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlags}, obj::{ObjInfo, ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlags, SymbolRef},
}; };
use crate::{ use crate::{
@ -52,7 +52,7 @@ pub struct DiffViewState {
#[derive(Default)] #[derive(Default)]
pub struct SymbolViewState { pub struct SymbolViewState {
pub highlighted_symbol: Option<String>, pub highlighted_symbol: (Option<SymbolRef>, Option<SymbolRef>),
pub selected_symbol: Option<SymbolRefByName>, pub selected_symbol: Option<SymbolRefByName>,
pub reverse_fn_order: bool, pub reverse_fn_order: bool,
pub disable_reverse_fn_order: bool, pub disable_reverse_fn_order: bool,
@ -184,6 +184,7 @@ fn symbol_ui(
section: Option<&ObjSection>, section: Option<&ObjSection>,
state: &mut SymbolViewState, state: &mut SymbolViewState,
appearance: &Appearance, appearance: &Appearance,
left: bool,
) -> Option<View> { ) -> Option<View> {
if symbol.flags.0.contains(ObjSymbolFlags::Hidden) && !state.show_hidden_symbols { if symbol.flags.0.contains(ObjSymbolFlags::Hidden) && !state.show_hidden_symbols {
return None; return None;
@ -193,8 +194,10 @@ fn symbol_ui(
let name: &str = let name: &str =
if let Some(demangled) = &symbol.demangled_name { demangled } else { &symbol.name }; if let Some(demangled) = &symbol.demangled_name { demangled } else { &symbol.name };
let mut selected = false; let mut selected = false;
if let Some(sym) = &state.highlighted_symbol { if let Some(sym_ref) =
selected = sym == &symbol.name; if left { state.highlighted_symbol.0 } else { state.highlighted_symbol.1 }
{
selected = symbol_diff.symbol_ref == sym_ref;
} }
write_text("[", appearance.text_color, &mut job, appearance.code_font.clone()); write_text("[", appearance.text_color, &mut job, appearance.code_font.clone());
if symbol.flags.0.contains(ObjSymbolFlags::Common) { if symbol.flags.0.contains(ObjSymbolFlags::Common) {
@ -245,7 +248,15 @@ fn symbol_ui(
} }
} }
} else if response.hovered() { } else if response.hovered() {
state.highlighted_symbol = Some(symbol.name.clone()); state.highlighted_symbol = if let Some(diff_symbol) = symbol_diff.diff_symbol {
if left {
(Some(symbol_diff.symbol_ref), Some(diff_symbol))
} else {
(Some(diff_symbol), Some(symbol_diff.symbol_ref))
}
} else {
(None, None)
};
} }
ret ret
} }
@ -267,6 +278,7 @@ fn symbol_list_ui(
state: &mut SymbolViewState, state: &mut SymbolViewState,
lower_search: &str, lower_search: &str,
appearance: &Appearance, appearance: &Appearance,
left: bool,
) -> Option<View> { ) -> Option<View> {
let mut ret = None; let mut ret = None;
ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| { ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| {
@ -277,13 +289,46 @@ fn symbol_list_ui(
if !obj.0.common.is_empty() { if !obj.0.common.is_empty() {
CollapsingHeader::new(".comm").default_open(true).show(ui, |ui| { CollapsingHeader::new(".comm").default_open(true).show(ui, |ui| {
for (symbol, symbol_diff) in obj.0.common.iter().zip(&obj.1.common) { for (symbol, symbol_diff) in obj.0.common.iter().zip(&obj.1.common) {
ret = ret.or(symbol_ui(ui, symbol, symbol_diff, None, state, appearance)); ret = ret.or(symbol_ui(
ui,
symbol,
symbol_diff,
None,
state,
appearance,
left,
));
} }
}); });
} }
for (section, section_diff) in obj.0.sections.iter().zip(&obj.1.sections) { for (section, section_diff) in obj.0.sections.iter().zip(&obj.1.sections) {
CollapsingHeader::new(format!("{} ({:x})", section.name, section.size)) 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!("{match_percent:.0}%"),
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_source(Id::new(section.name.clone()).with(section.orig_index)) .id_source(Id::new(section.name.clone()).with(section.orig_index))
.default_open(true) .default_open(true)
.show(ui, |ui| { .show(ui, |ui| {
@ -301,6 +346,7 @@ fn symbol_list_ui(
Some(section), Some(section),
state, state,
appearance, appearance,
left,
)); ));
} }
} else { } else {
@ -317,6 +363,7 @@ fn symbol_list_ui(
Some(section), Some(section),
state, state,
appearance, appearance,
left,
)); ));
} }
} }
@ -447,6 +494,7 @@ pub fn symbol_diff_ui(ui: &mut Ui, state: &mut DiffViewState, appearance: &Appea
symbol_state, symbol_state,
&lower_search, &lower_search,
appearance, appearance,
true,
)); ));
} else { } else {
missing_obj_ui(ui, appearance); missing_obj_ui(ui, appearance);
@ -466,6 +514,7 @@ pub fn symbol_diff_ui(ui: &mut Ui, state: &mut DiffViewState, appearance: &Appea
symbol_state, symbol_state,
&lower_search, &lower_search,
appearance, appearance,
false,
)); ));
} else { } else {
missing_obj_ui(ui, appearance); missing_obj_ui(ui, appearance);