Initial commit

This commit is contained in:
2022-09-08 17:19:20 -04:00
commit cb3c6062c7
22 changed files with 5061 additions and 0 deletions

266
src/app.rs Normal file
View File

@@ -0,0 +1,266 @@
use std::{
default::Default,
ffi::OsStr,
path::{Path, PathBuf},
sync::{
atomic::{AtomicBool, Ordering},
Arc, RwLock,
},
time::Duration,
};
use eframe::Frame;
use notify::{RecursiveMode, Watcher};
use crate::{
jobs::{
build::{queue_build, BuildResult},
Job, JobResult, JobState,
},
views::{
config::config_ui, function_diff::function_diff_ui, jobs::jobs_ui,
symbol_diff::symbol_diff_ui,
},
};
#[derive(Default, Eq, PartialEq)]
pub enum View {
#[default]
SymbolDiff,
FunctionDiff,
}
#[derive(Default, serde::Deserialize, serde::Serialize)]
#[serde(default)]
pub struct ViewState {
#[serde(skip)]
pub jobs: Vec<JobState>,
#[serde(skip)]
pub build: Option<Box<BuildResult>>,
#[serde(skip)]
pub highlighted_symbol: Option<String>,
#[serde(skip)]
pub selected_symbol: Option<String>,
#[serde(skip)]
pub current_view: View,
// Config
pub reverse_fn_order: bool,
}
#[derive(Default, Clone, serde::Deserialize, serde::Serialize)]
#[serde(default)]
pub struct AppConfig {
pub project_dir: Option<PathBuf>,
pub build_asm_dir: Option<PathBuf>,
pub build_src_dir: Option<PathBuf>,
pub build_obj: Option<String>,
#[serde(skip)]
pub project_dir_change: bool,
}
/// We derive Deserialize/Serialize so we can persist app state on shutdown.
#[derive(serde::Deserialize, serde::Serialize)]
#[serde(default)]
pub struct App {
view_state: ViewState,
#[serde(skip)]
config: Arc<RwLock<AppConfig>>,
#[serde(skip)]
modified: Arc<AtomicBool>,
#[serde(skip)]
watcher: Option<notify::RecommendedWatcher>,
}
impl Default for App {
fn default() -> Self {
Self {
view_state: ViewState::default(),
config: Arc::new(Default::default()),
modified: Arc::new(Default::default()),
watcher: None,
}
}
}
const CONFIG_KEY: &str = "app_config";
impl App {
/// Called once before the first frame.
pub fn new(cc: &eframe::CreationContext<'_>) -> Self {
// This is also where you can customized the look at feel of egui using
// `cc.egui_ctx.set_visuals` and `cc.egui_ctx.set_fonts`.
// Load previous app state (if any).
// Note that you must enable the `persistence` feature for this to work.
if let Some(storage) = cc.storage {
let mut app: App = eframe::get_value(storage, eframe::APP_KEY).unwrap_or_default();
let mut config: AppConfig = eframe::get_value(storage, CONFIG_KEY).unwrap_or_default();
if config.project_dir.is_some() {
config.project_dir_change = true;
}
app.config = Arc::new(RwLock::new(config));
app
} else {
Self::default()
}
}
}
impl eframe::App for App {
/// Called each time the UI needs repainting, which may be many times per second.
/// Put your widgets into a `SidePanel`, `TopPanel`, `CentralPanel`, `Window` or `Area`.
fn update(&mut self, ctx: &egui::Context, frame: &mut Frame) {
let Self { config, view_state, .. } = self;
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
egui::menu::bar(ui, |ui| {
ui.menu_button("File", |ui| {
if ui.button("Quit").clicked() {
frame.close();
}
});
});
});
if view_state.current_view == View::FunctionDiff
&& matches!(&view_state.build, Some(b) if b.first_status.success && b.second_status.success)
{
egui::SidePanel::left("side_panel").show(ctx, |ui| {
if ui.button("Back").clicked() {
view_state.current_view = View::SymbolDiff;
}
ui.separator();
jobs_ui(ui, view_state);
});
egui::CentralPanel::default().show(ctx, |ui| {
function_diff_ui(ui, view_state);
});
} else {
egui::SidePanel::left("side_panel").show(ctx, |ui| {
ui.heading("Config");
config_ui(ui, config, view_state);
jobs_ui(ui, view_state);
});
egui::CentralPanel::default().show(ctx, |ui| {
symbol_diff_ui(ui, view_state);
});
}
if view_state.jobs.iter().any(|job| {
if let Some(handle) = &job.handle {
return !handle.is_finished();
}
false
}) {
ctx.request_repaint();
} else {
ctx.request_repaint();
ctx.request_repaint_after(Duration::from_millis(100));
}
}
/// Called by the frame work to save state before shutdown.
fn save(&mut self, storage: &mut dyn eframe::Storage) {
if let Ok(config) = self.config.read() {
eframe::set_value(storage, CONFIG_KEY, &*config);
}
eframe::set_value(storage, eframe::APP_KEY, self);
}
fn post_rendering(&mut self, _window_size_px: [u32; 2], _frame: &Frame) {
for job in &mut self.view_state.jobs {
if let Some(handle) = &job.handle {
if !handle.is_finished() {
continue;
}
match job.handle.take().unwrap().join() {
Ok(result) => {
log::info!("Job {} finished", job.id);
match result {
JobResult::None => {
if let Some(err) = &job.status.read().unwrap().error {
log::error!("{:?}", err);
}
}
JobResult::Build(state) => {
self.view_state.build = Some(state);
}
}
}
Err(e) => {
log::error!("Failed to join job handle: {:?}", e);
}
}
}
}
if self.view_state.jobs.iter().any(|v| v.should_remove) {
let mut i = 0;
while i < self.view_state.jobs.len() {
let job = &self.view_state.jobs[i];
if job.should_remove && job.handle.is_none() {
self.view_state.jobs.remove(i);
} else {
i += 1;
}
}
}
if let Ok(mut config) = self.config.write() {
if config.project_dir_change {
drop(self.watcher.take());
if let Some(project_dir) = &config.project_dir {
match create_watcher(self.modified.clone(), project_dir) {
Ok(watcher) => self.watcher = Some(watcher),
Err(e) => eprintln!("Failed to create watcher: {}", e),
}
config.project_dir_change = false;
self.modified.store(true, Ordering::Relaxed);
}
}
if let Some(build_obj) = &config.build_obj {
if self.modified.load(Ordering::Relaxed) {
if !self
.view_state
.jobs
.iter()
.any(|j| j.job_type == Job::Build && j.handle.is_some())
{
self.view_state
.jobs
.push(queue_build(build_obj.clone(), self.config.clone()));
}
self.modified.store(false, Ordering::Relaxed);
}
}
}
}
}
fn create_watcher(
modified: Arc<AtomicBool>,
project_dir: &Path,
) -> notify::Result<notify::RecommendedWatcher> {
let mut watcher =
notify::recommended_watcher(move |res: notify::Result<notify::Event>| match res {
Ok(event) => {
if matches!(event.kind, notify::EventKind::Modify(..)) {
let watch_extensions = &[
Some(OsStr::new("c")),
Some(OsStr::new("cp")),
Some(OsStr::new("cpp")),
Some(OsStr::new("h")),
Some(OsStr::new("hpp")),
];
if event.paths.iter().any(|p| watch_extensions.contains(&p.extension())) {
modified.store(true, Ordering::Relaxed);
}
}
}
Err(e) => println!("watch error: {:?}", e),
})?;
watcher.watch(project_dir, RecursiveMode::Recursive)?;
Ok(watcher)
}

396
src/diff.rs Normal file
View File

@@ -0,0 +1,396 @@
use std::collections::BTreeMap;
use anyhow::Result;
use ppc750cl::{disasm_iter, Argument};
use crate::{
editops::{editops_find, LevEditType},
obj::{
ObjInfo, ObjIns, ObjInsArg, ObjInsArgDiff, ObjInsBranchFrom, ObjInsBranchTo, ObjInsDiff,
ObjInsDiffKind, ObjReloc, ObjRelocKind, ObjSection, ObjSectionKind, ObjSymbol,
ObjSymbolFlags,
},
};
// Relative relocation, can be Simm or BranchDest
fn is_relative_arg(arg: &ObjInsArg) -> bool {
matches!(arg, ObjInsArg::Arg(arg) if matches!(arg, Argument::Simm(_) | Argument::BranchDest(_)))
}
// Relative or absolute relocation, can be Uimm, Simm or Offset
fn is_rel_abs_arg(arg: &ObjInsArg) -> bool {
matches!(arg, ObjInsArg::Arg(arg) if matches!(arg, Argument::Uimm(_) | Argument::Simm(_) | Argument::Offset(_)))
}
fn is_offset_arg(arg: &ObjInsArg) -> bool { matches!(arg, ObjInsArg::Arg(Argument::Offset(_))) }
fn process_code(data: &[u8], address: u64, relocs: &[ObjReloc]) -> Result<(Vec<u8>, Vec<ObjIns>)> {
let ins_count = data.len() / 4;
let mut ops = Vec::<u8>::with_capacity(ins_count);
let mut insts = Vec::<ObjIns>::with_capacity(ins_count);
for mut ins in disasm_iter(data, address as u32) {
let reloc = relocs.iter().find(|r| (r.address as u32 & !3) == ins.addr);
if let Some(reloc) = reloc {
// Zero out relocations
ins.code = match reloc.kind {
ObjRelocKind::PpcEmbSda21 => ins.code & !0x1FFFFF,
ObjRelocKind::PpcRel24 => ins.code & !0x3FFFFFC,
ObjRelocKind::PpcRel14 => ins.code & !0xFFFC,
ObjRelocKind::PpcAddr16Hi
| ObjRelocKind::PpcAddr16Ha
| ObjRelocKind::PpcAddr16Lo => ins.code & !0xFFFF,
_ => ins.code,
};
}
let simplified = ins.simplified();
let mut args: Vec<ObjInsArg> =
simplified.args.iter().map(|a| ObjInsArg::Arg(a.clone())).collect();
if let Some(reloc) = reloc {
match reloc.kind {
ObjRelocKind::PpcEmbSda21 => {
args = vec![args[0].clone(), ObjInsArg::Reloc];
}
ObjRelocKind::PpcRel24 | ObjRelocKind::PpcRel14 => {
let arg = args
.iter_mut()
.rfind(|a| is_relative_arg(a))
.ok_or_else(|| anyhow::Error::msg("Failed to locate rel arg for reloc"))?;
*arg = ObjInsArg::Reloc;
}
ObjRelocKind::PpcAddr16Hi
| ObjRelocKind::PpcAddr16Ha
| ObjRelocKind::PpcAddr16Lo => {
let arg = args.iter_mut().rfind(|a| is_rel_abs_arg(a)).ok_or_else(|| {
anyhow::Error::msg("Failed to locate rel/abs arg for reloc")
})?;
*arg =
if is_offset_arg(arg) { ObjInsArg::RelocOffset } else { ObjInsArg::Reloc };
}
_ => {}
}
}
ops.push(simplified.ins.op as u8);
let suffix = simplified.ins.suffix();
insts.push(ObjIns {
ins: simplified.ins,
mnemonic: format!("{}{}", simplified.mnemonic, suffix),
args,
reloc: reloc.cloned(),
});
}
Ok((ops, insts))
}
pub fn diff_code(
left_data: &[u8],
right_data: &[u8],
left_symbol: &mut ObjSymbol,
right_symbol: &mut ObjSymbol,
left_relocs: &[ObjReloc],
right_relocs: &[ObjReloc],
) -> Result<()> {
let left_code =
&left_data[left_symbol.address as usize..(left_symbol.address + left_symbol.size) as usize];
let (left_ops, left_insts) = process_code(left_code, left_symbol.address, left_relocs)?;
let right_code = &right_data
[right_symbol.address as usize..(right_symbol.address + right_symbol.size) as usize];
let (right_ops, right_insts) = process_code(right_code, right_symbol.address, right_relocs)?;
let mut left_diff = Vec::<ObjInsDiff>::new();
let mut right_diff = Vec::<ObjInsDiff>::new();
let edit_ops = editops_find(&left_ops, &right_ops);
{
let mut op_iter = edit_ops.iter();
let mut left_iter = left_insts.iter();
let mut right_iter = right_insts.iter();
let mut cur_op = op_iter.next();
let mut cur_left = left_iter.next();
let mut cur_right = right_iter.next();
while let Some(op) = cur_op {
let left_addr = op.first_start as u32 * 4;
let right_addr = op.second_start as u32 * 4;
while let (Some(left), Some(right)) = (cur_left, cur_right) {
if (left.ins.addr - left_symbol.address as u32) < left_addr {
left_diff.push(ObjInsDiff { ins: Some(left.clone()), ..ObjInsDiff::default() });
right_diff
.push(ObjInsDiff { ins: Some(right.clone()), ..ObjInsDiff::default() });
} else {
break;
}
cur_left = left_iter.next();
cur_right = right_iter.next();
}
if let (Some(left), Some(right)) = (cur_left, cur_right) {
if (left.ins.addr - left_symbol.address as u32) != left_addr {
return Err(anyhow::Error::msg("Instruction address mismatch (left)"));
}
if (right.ins.addr - right_symbol.address as u32) != right_addr {
return Err(anyhow::Error::msg("Instruction address mismatch (right)"));
}
match op.op_type {
LevEditType::Replace => {
left_diff
.push(ObjInsDiff { ins: Some(left.clone()), ..ObjInsDiff::default() });
right_diff
.push(ObjInsDiff { ins: Some(right.clone()), ..ObjInsDiff::default() });
cur_left = left_iter.next();
cur_right = right_iter.next();
}
LevEditType::Insert => {
left_diff.push(ObjInsDiff::default());
right_diff
.push(ObjInsDiff { ins: Some(right.clone()), ..ObjInsDiff::default() });
cur_right = right_iter.next();
}
LevEditType::Delete => {
left_diff
.push(ObjInsDiff { ins: Some(left.clone()), ..ObjInsDiff::default() });
right_diff.push(ObjInsDiff::default());
cur_left = left_iter.next();
}
LevEditType::Keep => unreachable!(),
}
} else {
break;
}
cur_op = op_iter.next();
}
// Finalize
while cur_left.is_some() || cur_right.is_some() {
left_diff.push(ObjInsDiff { ins: cur_left.cloned(), ..ObjInsDiff::default() });
right_diff.push(ObjInsDiff { ins: cur_right.cloned(), ..ObjInsDiff::default() });
cur_left = left_iter.next();
cur_right = right_iter.next();
}
}
resolve_branches(&mut left_diff);
resolve_branches(&mut right_diff);
let mut diff_state = InsDiffState::default();
for (left, right) in left_diff.iter_mut().zip(right_diff.iter_mut()) {
let result = compare_ins(left, right, &mut diff_state)?;
left.kind = result.kind;
right.kind = result.kind;
left.arg_diff = result.left_args_diff;
right.arg_diff = result.right_args_diff;
}
let total = left_insts.len();
let percent = ((total - diff_state.diff_count) as f32 / total as f32) * 100.0;
left_symbol.match_percent = percent;
right_symbol.match_percent = percent;
left_symbol.instructions = left_diff;
right_symbol.instructions = right_diff;
Ok(())
}
fn resolve_branches(vec: &mut [ObjInsDiff]) {
let mut branch_idx = 0usize;
// Map addresses to indices
let mut addr_map = BTreeMap::<u32, usize>::new();
for (i, ins_diff) in vec.iter().enumerate() {
if let Some(ins) = &ins_diff.ins {
addr_map.insert(ins.ins.addr, i);
}
}
// Generate branches
let mut branches = BTreeMap::<usize, ObjInsBranchFrom>::new();
for (i, ins_diff) in vec.iter_mut().enumerate() {
if let Some(ins) = &ins_diff.ins {
if ins.ins.is_blr() || ins.reloc.is_some() {
continue;
}
if let Some(ins_idx) = ins.ins.branch_dest().and_then(|dest| addr_map.get(&dest)) {
if let Some(branch) = branches.get_mut(ins_idx) {
ins_diff.branch_to =
Some(ObjInsBranchTo { ins_idx: *ins_idx, branch_idx: branch.branch_idx });
branch.ins_idx.push(i);
} else {
ins_diff.branch_to = Some(ObjInsBranchTo { ins_idx: *ins_idx, branch_idx });
branches.insert(*ins_idx, ObjInsBranchFrom { ins_idx: vec![i], branch_idx });
branch_idx += 1;
}
}
}
}
// Store branch from
for (i, branch) in branches {
vec[i].branch_from = Some(branch);
}
}
fn reloc_eq(left_reloc: Option<&ObjReloc>, right_reloc: Option<&ObjReloc>) -> bool {
if let (Some(left), Some(right)) = (left_reloc, right_reloc) {
if left.kind != right.kind {
return false;
}
let name_matches = left.target.name == right.target.name;
match (&left.target_section, &right.target_section) {
(Some(sl), Some(sr)) => {
// Match if section and name or address match
sl == sr && (name_matches || left.target.address == right.target.address)
}
(Some(_), None) => false,
(None, Some(_)) => {
// Match if possibly stripped weak symbol
name_matches && right.target.flags.0.contains(ObjSymbolFlags::Weak)
}
(None, None) => name_matches,
}
} else {
false
}
}
fn arg_eq(
left: &ObjInsArg,
right: &ObjInsArg,
left_diff: &ObjInsDiff,
right_diff: &ObjInsDiff,
) -> bool {
return match left {
ObjInsArg::Arg(l) => match right {
ObjInsArg::Arg(r) => match r {
Argument::BranchDest(_) => {
// Compare dest instruction idx after diffing
left_diff.branch_to.as_ref().map(|b| b.ins_idx)
== right_diff.branch_to.as_ref().map(|b| b.ins_idx)
}
_ => format!("{}", l) == format!("{}", r),
},
_ => false,
},
ObjInsArg::Reloc => {
matches!(right, ObjInsArg::Reloc)
&& reloc_eq(
left_diff.ins.as_ref().and_then(|i| i.reloc.as_ref()),
right_diff.ins.as_ref().and_then(|i| i.reloc.as_ref()),
)
}
ObjInsArg::RelocOffset => {
matches!(right, ObjInsArg::RelocOffset)
&& reloc_eq(
left_diff.ins.as_ref().and_then(|i| i.reloc.as_ref()),
right_diff.ins.as_ref().and_then(|i| i.reloc.as_ref()),
)
}
};
}
#[derive(Default)]
struct InsDiffState {
diff_count: usize,
left_arg_idx: usize,
right_arg_idx: usize,
left_args_idx: BTreeMap<String, usize>,
right_args_idx: BTreeMap<String, usize>,
}
#[derive(Default)]
struct InsDiffResult {
kind: ObjInsDiffKind,
left_args_diff: Vec<Option<ObjInsArgDiff>>,
right_args_diff: Vec<Option<ObjInsArgDiff>>,
}
fn compare_ins(
left: &ObjInsDiff,
right: &ObjInsDiff,
state: &mut InsDiffState,
) -> Result<InsDiffResult> {
let mut result = InsDiffResult::default();
if let (Some(left_ins), Some(right_ins)) = (&left.ins, &right.ins) {
if left_ins.args.len() != right_ins.args.len() || left_ins.ins.op != right_ins.ins.op {
// Totally different op
result.kind = ObjInsDiffKind::Replace;
state.diff_count += 1;
return Ok(result);
}
if left_ins.mnemonic != right_ins.mnemonic {
// Same op but different mnemonic, still cmp args
result.kind = ObjInsDiffKind::OpMismatch;
state.diff_count += 1;
}
for (a, b) in left_ins.args.iter().zip(&right_ins.args) {
if arg_eq(a, b, left, right) {
result.left_args_diff.push(None);
result.right_args_diff.push(None);
} else {
if result.kind == ObjInsDiffKind::None {
result.kind = ObjInsDiffKind::ArgMismatch;
state.diff_count += 1;
}
let a_str = match a {
ObjInsArg::Arg(arg) => format!("{}", arg),
ObjInsArg::Reloc | ObjInsArg::RelocOffset => String::new(),
};
let a_diff = if let Some(idx) = state.left_args_idx.get(&a_str) {
ObjInsArgDiff { idx: *idx }
} else {
let idx = state.left_arg_idx;
state.left_args_idx.insert(a_str, idx);
state.left_arg_idx += 1;
ObjInsArgDiff { idx }
};
let b_str = match b {
ObjInsArg::Arg(arg) => format!("{}", arg),
ObjInsArg::Reloc | ObjInsArg::RelocOffset => String::new(),
};
let b_diff = if let Some(idx) = state.right_args_idx.get(&b_str) {
ObjInsArgDiff { idx: *idx }
} else {
let idx = state.right_arg_idx;
state.right_args_idx.insert(b_str, idx);
state.right_arg_idx += 1;
ObjInsArgDiff { idx }
};
result.left_args_diff.push(Some(a_diff));
result.right_args_diff.push(Some(b_diff));
}
}
} else if left.ins.is_some() {
result.kind = ObjInsDiffKind::Delete;
state.diff_count += 1;
} else {
result.kind = ObjInsDiffKind::Insert;
state.diff_count += 1;
}
Ok(result)
}
fn find_section<'a>(obj: &'a mut ObjInfo, name: &str) -> Option<&'a mut ObjSection> {
obj.sections.iter_mut().find(|s| s.name == name)
}
fn find_symbol<'a>(symbols: &'a mut [ObjSymbol], name: &str) -> Option<&'a mut ObjSymbol> {
symbols.iter_mut().find(|s| s.name == name)
}
pub fn diff_objs(left: &mut ObjInfo, right: &mut ObjInfo) -> Result<()> {
for left_section in &mut left.sections {
if let Some(right_section) = find_section(right, &left_section.name) {
for left_symbol in &mut left_section.symbols {
if let Some(right_symbol) =
find_symbol(&mut right_section.symbols, &left_symbol.name)
{
left_symbol.diff_symbol = Some(right_symbol.name.clone());
right_symbol.diff_symbol = Some(left_symbol.name.clone());
if left_section.kind == ObjSectionKind::Code {
diff_code(
&left_section.data,
&right_section.data,
left_symbol,
right_symbol,
&left_section.relocations,
&right_section.relocations,
)?;
}
}
}
}
}
Ok(())
}

253
src/editops.rs Normal file
View File

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

242
src/elf.rs Normal file
View File

@@ -0,0 +1,242 @@
use std::{fs, path::Path};
use anyhow::{Context, Result};
use cwdemangle::demangle;
use flagset::Flags;
use object::{
Object, ObjectSection, ObjectSymbol, RelocationKind, RelocationTarget, SectionKind, SymbolKind,
SymbolSection,
};
use crate::obj::{
ObjInfo, ObjReloc, ObjRelocKind, ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlagSet,
ObjSymbolFlags,
};
fn to_obj_section_kind(kind: SectionKind) -> ObjSectionKind {
match kind {
SectionKind::Text => ObjSectionKind::Code,
SectionKind::Data | SectionKind::ReadOnlyData => ObjSectionKind::Data,
SectionKind::UninitializedData => ObjSectionKind::Bss,
_ => panic!("Unhandled section kind {:?}", kind),
}
}
fn to_obj_symbol(symbol: &object::Symbol<'_, '_>) -> Result<ObjSymbol> {
let mut name = symbol.name().context("Failed to process symbol name")?;
if name.is_empty() {
println!("Found empty sym: {:?}", symbol);
name = "?";
}
let mut flags = ObjSymbolFlagSet(ObjSymbolFlags::none());
if symbol.is_global() {
flags = ObjSymbolFlagSet(flags.0 | ObjSymbolFlags::Global);
}
if symbol.is_local() {
flags = ObjSymbolFlagSet(flags.0 | ObjSymbolFlags::Local);
}
if symbol.is_common() {
flags = ObjSymbolFlagSet(flags.0 | ObjSymbolFlags::Common);
}
if symbol.is_weak() {
flags = ObjSymbolFlagSet(flags.0 | ObjSymbolFlags::Weak);
}
Ok(ObjSymbol {
name: name.to_string(),
demangled_name: demangle(name),
address: symbol.address(),
size: symbol.size(),
size_known: symbol.size() != 0,
flags,
diff_symbol: None,
instructions: vec![],
match_percent: 0.0,
})
}
const R_PPC_ADDR16_LO: u32 = 4;
const R_PPC_ADDR16_HI: u32 = 5;
const R_PPC_ADDR16_HA: u32 = 6;
const R_PPC_REL24: u32 = 10;
const R_PPC_REL14: u32 = 11;
const R_PPC_EMB_SDA21: u32 = 109;
fn filter_sections(obj_file: &object::File<'_>) -> Result<Vec<ObjSection>> {
let mut result = Vec::<ObjSection>::new();
for section in obj_file.sections() {
if section.size() == 0 {
continue;
}
if section.kind() != SectionKind::Text
&& section.kind() != SectionKind::Data
&& section.kind() != SectionKind::ReadOnlyData
&& section.kind() != SectionKind::UninitializedData
{
continue;
}
let name = section.name().context("Failed to process section name")?;
let data = section.data().context("Failed to read section data")?;
result.push(ObjSection {
name: name.to_string(),
kind: to_obj_section_kind(section.kind()),
address: section.address(),
size: section.size(),
data: data.to_vec(),
index: section.index().0,
symbols: Vec::new(),
relocations: Vec::new(),
});
}
result.sort_by(|a, b| a.name.cmp(&b.name));
Ok(result)
}
fn symbols_by_section(obj_file: &object::File<'_>, section: &ObjSection) -> Result<Vec<ObjSymbol>> {
let mut result = Vec::<ObjSymbol>::new();
for symbol in obj_file.symbols() {
if symbol.kind() == SymbolKind::Section {
continue;
}
if let Some(index) = symbol.section().index() {
if index.0 == section.index {
if symbol.is_local() && section.kind == ObjSectionKind::Code {
// TODO strip local syms in diff?
let name = symbol.name().context("Failed to process symbol name")?;
if name.starts_with("lbl_") {
continue;
}
}
result.push(to_obj_symbol(&symbol)?);
}
}
}
result.sort_by_key(|v| v.address);
let mut iter = result.iter_mut().peekable();
while let Some(symbol) = iter.next() {
if symbol.size == 0 {
if let Some(next_symbol) = iter.peek() {
symbol.size = next_symbol.address - symbol.address;
} else {
symbol.size = (section.address + section.size) - symbol.address;
}
}
}
Ok(result)
}
fn common_symbols(obj_file: &object::File<'_>) -> Result<Vec<ObjSymbol>> {
let mut result = Vec::<ObjSymbol>::new();
for symbol in obj_file.symbols() {
if symbol.is_common() {
result.push(to_obj_symbol(&symbol)?);
}
}
Ok(result)
}
fn locate_section_symbol(
obj_file: &object::File<'_>,
target: &object::Symbol<'_, '_>,
address: u64,
) -> Result<ObjSymbol> {
let section_index =
target.section_index().ok_or_else(|| anyhow::Error::msg("Unknown section index"))?;
for symbol in obj_file.symbols() {
if !matches!(symbol.section_index(), Some(idx) if idx == section_index) {
continue;
}
if symbol.kind() == SymbolKind::Section || symbol.address() != address {
continue;
}
return to_obj_symbol(&symbol);
}
Err(anyhow::Error::msg("Failed to locate reloc offset sym"))
}
fn relocations_by_section(
obj_file: &object::File<'_>,
section: &ObjSection,
) -> Result<Vec<ObjReloc>> {
let obj_section = obj_file
.section_by_name(&section.name)
.ok_or_else(|| anyhow::Error::msg("Failed to locate section"))?;
let mut relocations = Vec::<ObjReloc>::new();
for (address, reloc) in obj_section.relocations() {
let symbol = match reloc.target() {
RelocationTarget::Symbol(idx) => obj_file
.symbol_by_index(idx)
.context("Failed to locate relocation target symbol")?,
_ => {
return Err(anyhow::Error::msg(format!(
"Unhandled relocation target: {:?}",
reloc.target()
)));
}
};
let kind = match reloc.kind() {
RelocationKind::Absolute => ObjRelocKind::Absolute,
RelocationKind::Elf(kind) => match kind {
R_PPC_ADDR16_LO => ObjRelocKind::PpcAddr16Lo,
R_PPC_ADDR16_HI => ObjRelocKind::PpcAddr16Hi,
R_PPC_ADDR16_HA => ObjRelocKind::PpcAddr16Ha,
R_PPC_REL24 => ObjRelocKind::PpcRel24,
R_PPC_REL14 => ObjRelocKind::PpcRel14,
R_PPC_EMB_SDA21 => ObjRelocKind::PpcEmbSda21,
_ => {
return Err(anyhow::Error::msg(format!(
"Unhandled ELF relocation type: {}",
kind
)))
}
},
_ => {
return Err(anyhow::Error::msg(format!(
"Unhandled relocation type: {:?}",
reloc.kind()
)))
}
};
let target_section = match symbol.section() {
SymbolSection::Common => Some(".comm".to_string()),
SymbolSection::Section(idx) => obj_file
.section_by_index(idx)
.map(|s| s.name().map(|s| s.to_string()).ok())
.ok()
.flatten(),
_ => None,
};
// println!("Reloc: {:?}", reloc.addend());
let target = match symbol.kind() {
SymbolKind::Text | SymbolKind::Data | SymbolKind::Unknown => to_obj_symbol(&symbol),
SymbolKind::Section => {
let addend = reloc.addend();
if addend < 0 {
Err(anyhow::Error::msg(format!("Negative addend in section reloc: {}", addend)))
} else {
locate_section_symbol(obj_file, &symbol, addend as u64)
}
}
_ => Err(anyhow::Error::msg(format!(
"Unhandled relocation symbol type {:?}",
symbol.kind()
))),
}?;
relocations.push(ObjReloc { kind, address, target, target_section });
}
Ok(relocations)
}
pub fn read(obj_path: &Path) -> Result<ObjInfo> {
let bin_data = fs::read(obj_path)?;
let obj_file = object::File::parse(&*bin_data)?;
let mut result = ObjInfo {
path: obj_path.to_owned(),
sections: filter_sections(&obj_file)?,
common: common_symbols(&obj_file)?,
};
for section in &mut result.sections {
section.symbols = symbols_by_section(&obj_file, section)?;
section.relocations = relocations_by_section(&obj_file, section)?;
}
Ok(result)
}

107
src/jobs/build.rs Normal file
View File

@@ -0,0 +1,107 @@
use std::{
path::Path,
process::Command,
str::from_utf8,
sync::{mpsc::Receiver, Arc, RwLock},
};
use anyhow::{Context, Error, Result};
use crate::{
app::AppConfig,
diff::diff_objs,
elf,
jobs::{queue_job, update_status, Job, JobResult, JobState, Status},
obj::ObjInfo,
};
pub struct BuildStatus {
pub success: bool,
pub log: String,
}
pub struct BuildResult {
pub first_status: BuildStatus,
pub second_status: BuildStatus,
pub first_obj: Option<ObjInfo>,
pub second_obj: Option<ObjInfo>,
}
fn run_make(cwd: &Path, arg: &Path) -> BuildStatus {
match (|| -> Result<BuildStatus> {
let output = Command::new("make")
.current_dir(cwd)
.arg(arg)
.output()
.context("Failed to execute build")?;
let stdout = from_utf8(&output.stdout).context("Failed to process stdout")?;
let stderr = from_utf8(&output.stderr).context("Failed to process stderr")?;
Ok(BuildStatus {
success: output.status.code().unwrap_or(-1) == 0,
log: format!("{}\n{}", stdout, stderr),
})
})() {
Ok(status) => status,
Err(e) => BuildStatus { success: false, log: e.to_string() },
}
}
fn run_build(
status: &Status,
cancel: Receiver<()>,
obj_path: String,
config: Arc<RwLock<AppConfig>>,
) -> Result<Box<BuildResult>> {
let config = config.read().map_err(|_| Error::msg("Failed to lock app config"))?.clone();
let project_dir =
config.project_dir.as_ref().ok_or_else(|| Error::msg("Missing project dir"))?;
let mut asm_path = config
.build_asm_dir
.as_ref()
.ok_or_else(|| Error::msg("Missing build asm dir"))?
.to_owned();
asm_path.push(&obj_path);
let mut src_path = config
.build_src_dir
.as_ref()
.ok_or_else(|| Error::msg("Missing build src dir"))?
.to_owned();
src_path.push(&obj_path);
let asm_path_rel =
asm_path.strip_prefix(project_dir).context("Failed to create relative asm obj path")?;
let src_path_rel =
src_path.strip_prefix(project_dir).context("Failed to create relative src obj path")?;
update_status(status, format!("Building asm {}", obj_path), 0, 5, &cancel)?;
let first_status = run_make(project_dir, asm_path_rel);
update_status(status, format!("Building src {}", obj_path), 1, 5, &cancel)?;
let second_status = run_make(project_dir, src_path_rel);
let mut first_obj = if first_status.success {
update_status(status, format!("Loading asm {}", obj_path), 2, 5, &cancel)?;
Some(elf::read(&asm_path)?)
} else {
None
};
let mut second_obj = if second_status.success {
update_status(status, format!("Loading src {}", obj_path), 3, 5, &cancel)?;
Some(elf::read(&src_path)?)
} else {
None
};
if let (Some(first_obj), Some(second_obj)) = (&mut first_obj, &mut second_obj) {
update_status(status, "Performing diff".to_string(), 4, 5, &cancel)?;
diff_objs(first_obj, second_obj)?;
}
update_status(status, "Complete".to_string(), 5, 5, &cancel)?;
Ok(Box::new(BuildResult { first_status, second_status, first_obj, second_obj }))
}
pub fn queue_build(obj_path: String, config: Arc<RwLock<AppConfig>>) -> JobState {
queue_job(Job::Build, move |status, cancel| {
run_build(status, cancel, obj_path, config).map(JobResult::Build)
})
}

104
src/jobs/mod.rs Normal file
View File

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

11
src/lib.rs Normal file
View File

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

31
src/main.rs Normal file
View File

@@ -0,0 +1,31 @@
#![warn(clippy::all, rust_2018_idioms)]
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
// When compiling natively:
#[cfg(not(target_arch = "wasm32"))]
fn main() {
// Log to stdout (if you run with `RUST_LOG=debug`).
tracing_subscriber::fmt::init();
let native_options = eframe::NativeOptions::default();
// native_options.renderer = eframe::Renderer::Wgpu;
eframe::run_native("objdiff", native_options, Box::new(|cc| Box::new(objdiff::App::new(cc))));
}
// when compiling to web using trunk.
#[cfg(target_arch = "wasm32")]
fn main() {
// Make sure panics are logged using `console.error`.
console_error_panic_hook::set_once();
// Redirect tracing to console.log and friends:
tracing_wasm::set_as_global_default();
let web_options = eframe::WebOptions::default();
eframe::start_web(
"the_canvas_id", // hardcode it
web_options,
Box::new(|cc| Box::new(eframe_template::TemplateApp::new(cc))),
)
.expect("failed to start eframe");
}

132
src/obj.rs Normal file
View File

@@ -0,0 +1,132 @@
use std::path::PathBuf;
use flagset::{flags, FlagSet};
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
pub enum ObjSectionKind {
Code,
Data,
Bss,
}
flags! {
pub enum ObjSymbolFlags: u8 {
Global,
Local,
Weak,
Common,
}
}
#[derive(Debug, Copy, Clone, Default)]
pub struct ObjSymbolFlagSet(pub(crate) FlagSet<ObjSymbolFlags>);
#[derive(Debug, Clone)]
pub struct ObjSection {
pub name: String,
pub kind: ObjSectionKind,
pub address: u64,
pub size: u64,
pub data: Vec<u8>,
pub index: usize,
pub symbols: Vec<ObjSymbol>,
pub relocations: Vec<ObjReloc>,
}
#[derive(Debug, Clone)]
pub enum ObjInsArg {
Arg(ppc750cl::Argument),
Reloc,
RelocOffset,
}
#[derive(Debug, Copy, Clone)]
pub struct ObjInsArgDiff {
/// Incrementing index for coloring
pub idx: usize,
}
#[derive(Debug, Clone)]
pub struct ObjInsBranchFrom {
/// Source instruction indices
pub ins_idx: Vec<usize>,
/// Incrementing index for coloring
pub branch_idx: usize,
}
#[derive(Debug, Clone)]
pub struct ObjInsBranchTo {
/// Target instruction index
pub ins_idx: usize,
/// Incrementing index for coloring
pub branch_idx: usize,
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
pub enum ObjInsDiffKind {
#[default]
None,
OpMismatch,
ArgMismatch,
Replace,
Delete,
Insert,
}
#[derive(Debug, Clone)]
pub struct ObjIns {
pub ins: ppc750cl::Ins,
pub mnemonic: String,
pub args: Vec<ObjInsArg>,
pub reloc: Option<ObjReloc>,
}
#[derive(Debug, Clone, Default)]
pub struct ObjInsDiff {
pub ins: Option<ObjIns>,
/// Diff kind
pub kind: ObjInsDiffKind,
/// Branches from instruction
pub branch_from: Option<ObjInsBranchFrom>,
/// Branches to instruction
pub branch_to: Option<ObjInsBranchTo>,
/// Arg diffs
pub arg_diff: Vec<Option<ObjInsArgDiff>>,
}
#[derive(Debug, Clone)]
pub struct ObjSymbol {
pub name: String,
pub demangled_name: Option<String>,
pub address: u64,
pub size: u64,
pub size_known: bool,
pub flags: ObjSymbolFlagSet,
// Diff
pub diff_symbol: Option<String>,
pub instructions: Vec<ObjInsDiff>,
pub match_percent: f32,
}
#[derive(Debug, Clone)]
pub struct ObjInfo {
pub path: PathBuf,
pub sections: Vec<ObjSection>,
pub common: Vec<ObjSymbol>,
}
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
pub enum ObjRelocKind {
Absolute,
PpcAddr16Hi,
PpcAddr16Ha,
PpcAddr16Lo,
// PpcAddr32,
// PpcRel32,
// PpcAddr24,
PpcRel24,
// PpcAddr14,
PpcRel14,
PpcEmbSda21,
}
#[derive(Debug, Clone)]
pub struct ObjReloc {
pub kind: ObjRelocKind,
pub address: u64,
pub target: ObjSymbol,
pub target_section: Option<String>,
}
// #[derive(Debug, Clone)]
// pub struct ObjInsDiff {
// pub kind: ObjInsDiffKind,
// pub left: Option<ObjIns>,
// pub right: Option<ObjIns>,
// }

87
src/views/config.rs Normal file
View File

@@ -0,0 +1,87 @@
use std::sync::{Arc, RwLock};
use crate::{
app::{AppConfig, ViewState},
jobs::build::queue_build,
};
pub fn config_ui(ui: &mut egui::Ui, config: &Arc<RwLock<AppConfig>>, view_state: &mut ViewState) {
let mut config_guard = config.write().unwrap();
let AppConfig { project_dir, project_dir_change, build_asm_dir, build_src_dir, build_obj } =
&mut *config_guard;
if ui.button("Select project dir").clicked() {
if let Some(path) = rfd::FileDialog::new().pick_folder() {
*project_dir = Some(path);
*project_dir_change = true;
*build_asm_dir = None;
*build_src_dir = None;
*build_obj = None;
}
}
if let Some(dir) = project_dir {
ui.label(dir.to_string_lossy());
}
ui.separator();
if let Some(project_dir) = project_dir {
if ui.button("Select asm build dir").clicked() {
if let Some(path) = rfd::FileDialog::new().set_directory(&project_dir).pick_folder() {
*build_asm_dir = Some(path);
*build_obj = None;
}
}
if let Some(dir) = build_asm_dir {
ui.label(dir.to_string_lossy());
}
ui.separator();
if ui.button("Select src build dir").clicked() {
if let Some(path) = rfd::FileDialog::new().set_directory(&project_dir).pick_folder() {
*build_src_dir = Some(path);
*build_obj = None;
}
}
if let Some(dir) = build_src_dir {
ui.label(dir.to_string_lossy());
}
ui.separator();
}
if let Some(build_src_dir) = build_src_dir {
if ui.button("Select obj").clicked() {
if let Some(path) = rfd::FileDialog::new()
.set_directory(&build_src_dir)
.add_filter("Object file", &["o"])
.pick_file()
{
let mut new_build_obj: Option<String> = None;
if let Ok(obj_path) = path.strip_prefix(&build_src_dir) {
new_build_obj = Some(obj_path.display().to_string());
} else if let Some(build_asm_dir) = build_asm_dir {
if let Ok(obj_path) = path.strip_prefix(&build_asm_dir) {
new_build_obj = Some(obj_path.display().to_string());
}
}
if let Some(new_build_obj) = new_build_obj {
*build_obj = Some(new_build_obj.clone());
view_state.jobs.push(queue_build(new_build_obj, config.clone()));
}
}
}
if let Some(build_obj) = build_obj {
ui.label(&*build_obj);
if ui.button("Build").clicked() {
view_state.jobs.push(queue_build(build_obj.clone(), config.clone()));
}
}
ui.separator();
}
ui.checkbox(&mut view_state.reverse_fn_order, "Reverse function order (deferred)");
ui.separator();
}

338
src/views/function_diff.rs Normal file
View File

@@ -0,0 +1,338 @@
use std::default::Default;
use cwdemangle::demangle;
use egui::{text::LayoutJob, Color32, FontFamily, FontId, Label, Sense, TextFormat};
use egui_extras::{Size, StripBuilder, TableBuilder};
use ppc750cl::Argument;
use crate::{
app::ViewState,
obj::{
ObjInfo, ObjIns, ObjInsArg, ObjInsArgDiff, ObjInsDiff, ObjInsDiffKind, ObjReloc,
ObjRelocKind, ObjSymbol,
},
views::symbol_diff::match_color_for_symbol,
};
const FONT_SIZE: f32 = 14.0;
const FONT_ID: FontId = FontId::new(FONT_SIZE, FontFamily::Monospace);
const COLOR_RED: Color32 = Color32::from_rgb(200, 40, 41);
fn write_text(str: &str, color: Color32, job: &mut LayoutJob) {
job.append(str, 0.0, TextFormat { font_id: FONT_ID, color, ..Default::default() });
}
fn write_reloc(reloc: &ObjReloc, job: &mut LayoutJob) {
let name = reloc.target.demangled_name.as_ref().unwrap_or(&reloc.target.name);
write_text(name, Color32::LIGHT_GRAY, job);
match reloc.kind {
ObjRelocKind::PpcAddr16Lo => write_text("@l", Color32::GRAY, job),
ObjRelocKind::PpcAddr16Hi => write_text("@h", Color32::GRAY, job),
ObjRelocKind::PpcAddr16Ha => write_text("@ha", Color32::GRAY, job),
ObjRelocKind::PpcEmbSda21 => write_text("@sda21", Color32::GRAY, job),
_ => {}
};
}
fn write_ins(
ins: &ObjIns,
diff_kind: &ObjInsDiffKind,
args: &[Option<ObjInsArgDiff>],
base_addr: u32,
job: &mut LayoutJob,
) {
let base_color = match diff_kind {
ObjInsDiffKind::None | ObjInsDiffKind::OpMismatch | ObjInsDiffKind::ArgMismatch => {
Color32::GRAY
}
ObjInsDiffKind::Replace => Color32::LIGHT_BLUE,
ObjInsDiffKind::Delete => COLOR_RED,
ObjInsDiffKind::Insert => Color32::GREEN,
};
write_text(
&ins.mnemonic,
match diff_kind {
ObjInsDiffKind::OpMismatch => Color32::LIGHT_BLUE,
_ => base_color,
},
job,
);
let mut writing_offset = false;
for (i, arg) in ins.args.iter().enumerate() {
if i == 0 {
write_text(" ", base_color, job);
}
if i > 0 && !writing_offset {
write_text(", ", base_color, job);
}
let color = if let Some(diff) = args.get(i).and_then(|a| a.as_ref()) {
COLOR_ROTATION[diff.idx % COLOR_ROTATION.len()]
} else {
base_color
};
match arg {
ObjInsArg::Arg(arg) => match arg {
Argument::Offset(val) => {
write_text(&format!("{}", val), color, job);
write_text("(", base_color, job);
writing_offset = true;
continue;
}
Argument::BranchDest(dest) => {
let addr = dest.0 + ins.ins.addr as i32 - base_addr as i32;
write_text(&format!("{:x}", addr), color, job);
}
Argument::Uimm(_) | Argument::Simm(_) => {
write_text(&format!("{}", arg), color, job);
}
_ => {
write_text(&format!("{}", arg), color, job);
}
},
ObjInsArg::Reloc => {
write_reloc(ins.reloc.as_ref().unwrap(), job);
}
ObjInsArg::RelocOffset => {
write_reloc(ins.reloc.as_ref().unwrap(), job);
write_text("(", base_color, job);
writing_offset = true;
continue;
}
}
if writing_offset {
write_text(")", base_color, job);
writing_offset = false;
}
}
}
fn ins_hover_ui(ui: &mut egui::Ui, ins: &ObjIns) {
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false);
ui.label(format!("{:02X?}", ins.ins.code.to_be_bytes()));
for arg in &ins.args {
if let ObjInsArg::Arg(arg) = arg {
match arg {
Argument::Uimm(v) => {
ui.label(format!("{} == {}", v, v.0));
}
Argument::Simm(v) => {
ui.label(format!("{} == {}", v, v.0));
}
Argument::Offset(v) => {
ui.label(format!("{} == {}", v, v.0));
}
_ => {}
}
}
}
if let Some(reloc) = &ins.reloc {
ui.label(format!("Relocation type: {:?}", reloc.kind));
ui.colored_label(Color32::WHITE, format!("Name: {}", reloc.target.name));
if let Some(section) = &reloc.target_section {
ui.colored_label(Color32::WHITE, format!("Section: {}", section));
ui.colored_label(Color32::WHITE, format!("Address: {:x}", reloc.target.address));
ui.colored_label(Color32::WHITE, format!("Size: {:x}", reloc.target.size));
} else {
ui.colored_label(Color32::WHITE, "Extern".to_string());
}
}
});
}
fn ins_context_menu(ui: &mut egui::Ui, ins: &ObjIns) {
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false);
// if ui.button("Copy hex").clicked() {}
for arg in &ins.args {
if let ObjInsArg::Arg(arg) = arg {
match arg {
Argument::Uimm(v) => {
if ui.button(format!("Copy \"{}\"", v)).clicked() {
ui.output().copied_text = format!("{}", v);
ui.close_menu();
}
if ui.button(format!("Copy \"{}\"", v.0)).clicked() {
ui.output().copied_text = format!("{}", v.0);
ui.close_menu();
}
}
Argument::Simm(v) => {
if ui.button(format!("Copy \"{}\"", v)).clicked() {
ui.output().copied_text = format!("{}", v);
ui.close_menu();
}
if ui.button(format!("Copy \"{}\"", v.0)).clicked() {
ui.output().copied_text = format!("{}", v.0);
ui.close_menu();
}
}
Argument::Offset(v) => {
if ui.button(format!("Copy \"{}\"", v)).clicked() {
ui.output().copied_text = format!("{}", v);
ui.close_menu();
}
if ui.button(format!("Copy \"{}\"", v.0)).clicked() {
ui.output().copied_text = format!("{}", v.0);
ui.close_menu();
}
}
_ => {}
}
}
}
if let Some(reloc) = &ins.reloc {
if let Some(name) = &reloc.target.demangled_name {
if ui.button(format!("Copy \"{}\"", name)).clicked() {
ui.output().copied_text = name.clone();
ui.close_menu();
}
}
if ui.button(format!("Copy \"{}\"", reloc.target.name)).clicked() {
ui.output().copied_text = reloc.target.name.clone();
ui.close_menu();
}
}
});
}
const COLOR_ROTATION: [Color32; 9] = [
Color32::from_rgb(255, 0, 255),
Color32::from_rgb(0, 255, 255),
Color32::from_rgb(0, 128, 0),
Color32::from_rgb(255, 0, 0),
Color32::from_rgb(255, 255, 0),
Color32::from_rgb(255, 192, 203),
Color32::from_rgb(0, 0, 255),
Color32::from_rgb(0, 255, 0),
Color32::from_rgb(128, 128, 128),
];
fn find_symbol<'a>(obj: &'a ObjInfo, section_name: &str, name: &str) -> Option<&'a ObjSymbol> {
let section = obj.sections.iter().find(|s| s.name == section_name)?;
section.symbols.iter().find(|s| s.name == name)
}
fn asm_row_ui(ui: &mut egui::Ui, ins_diff: &ObjInsDiff, symbol: &ObjSymbol) {
if ins_diff.kind != ObjInsDiffKind::None {
ui.painter().rect_filled(ui.available_rect_before_wrap(), 0.0, ui.visuals().faint_bg_color);
}
let mut job = LayoutJob::default();
if let Some(ins) = &ins_diff.ins {
let base_color = match ins_diff.kind {
ObjInsDiffKind::None | ObjInsDiffKind::OpMismatch | ObjInsDiffKind::ArgMismatch => {
Color32::GRAY
}
ObjInsDiffKind::Replace => Color32::LIGHT_BLUE,
ObjInsDiffKind::Delete => COLOR_RED,
ObjInsDiffKind::Insert => Color32::GREEN,
};
write_text(
&format!("{:<6}", format!("{:x}:", ins.ins.addr - symbol.address as u32)),
base_color,
&mut job,
);
if let Some(branch) = &ins_diff.branch_from {
write_text("~> ", COLOR_ROTATION[branch.branch_idx % COLOR_ROTATION.len()], &mut job);
} else {
write_text(" ", base_color, &mut job);
}
write_ins(ins, &ins_diff.kind, &ins_diff.arg_diff, symbol.address as u32, &mut job);
if let Some(branch) = &ins_diff.branch_to {
write_text(" ~>", COLOR_ROTATION[branch.branch_idx % COLOR_ROTATION.len()], &mut job);
}
ui.add(Label::new(job).sense(Sense::click()))
.on_hover_ui_at_pointer(|ui| ins_hover_ui(ui, ins))
.context_menu(|ui| ins_context_menu(ui, ins));
} else {
ui.label("");
}
}
fn asm_table_ui(
table: TableBuilder<'_>,
left_obj: &ObjInfo,
right_obj: &ObjInfo,
fn_name: &str,
) -> Option<()> {
let left_symbol = find_symbol(left_obj, ".text", fn_name)?;
let right_symbol = find_symbol(right_obj, ".text", fn_name)?;
table.body(|body| {
body.rows(FONT_SIZE, left_symbol.instructions.len(), |row_index, mut row| {
row.col(|ui| {
asm_row_ui(ui, &left_symbol.instructions[row_index], left_symbol);
});
row.col(|ui| {
asm_row_ui(ui, &right_symbol.instructions[row_index], right_symbol);
});
});
});
Some(())
}
pub fn function_diff_ui(ui: &mut egui::Ui, view_state: &mut ViewState) {
if let (Some(result), Some(selected_symbol)) = (&view_state.build, &view_state.selected_symbol)
{
StripBuilder::new(ui).size(Size::exact(40.0)).size(Size::remainder()).vertical(
|mut strip| {
strip.strip(|builder| {
builder.sizes(Size::remainder(), 2).horizontal(|mut strip| {
let demangled = demangle(selected_symbol);
strip.cell(|ui| {
ui.scope(|ui| {
ui.style_mut().override_text_style =
Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false);
ui.colored_label(
Color32::WHITE,
demangled.as_ref().unwrap_or(selected_symbol),
);
ui.label("Diff asm:");
ui.separator();
});
});
strip.cell(|ui| {
ui.scope(|ui| {
ui.style_mut().override_text_style =
Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false);
if let Some(obj) = &result.second_obj {
if let Some(symbol) = find_symbol(obj, ".text", selected_symbol)
{
ui.colored_label(
match_color_for_symbol(symbol),
&format!("{:.0}%", symbol.match_percent),
);
}
}
ui.label("Diff src:");
ui.separator();
});
});
});
});
strip.cell(|ui| {
if let (Some(left_obj), Some(right_obj)) =
(&result.first_obj, &result.second_obj)
{
let table = TableBuilder::new(ui)
.striped(false)
.cell_layout(egui::Layout::left_to_right(egui::Align::Min))
.column(Size::relative(0.5))
.column(Size::relative(0.5))
.resizable(false);
asm_table_ui(table, left_obj, right_obj, selected_symbol);
}
});
},
);
}
}

38
src/views/jobs.rs Normal file
View File

@@ -0,0 +1,38 @@
use egui::{Color32, ProgressBar, Widget};
use crate::app::ViewState;
pub fn jobs_ui(ui: &mut egui::Ui, view_state: &ViewState) {
ui.label("Jobs");
for job in &view_state.jobs {
if let Ok(status) = job.status.read() {
ui.group(|ui| {
ui.label(&status.title);
let mut bar = ProgressBar::new(status.progress_percent);
if let Some(items) = &status.progress_items {
bar = bar.text(format!("{} / {}", items[0], items[1]));
}
bar.ui(ui);
const STATUS_LENGTH: usize = 80;
if let Some(err) = &status.error {
let err_string = err.to_string();
ui.colored_label(
Color32::from_rgb(255, 0, 0),
if err_string.len() > STATUS_LENGTH - 10 {
format!("Error: {}...", &err_string[0..STATUS_LENGTH - 10])
} else {
format!("Error: {:width$}", err_string, width = STATUS_LENGTH - 7)
},
);
} else {
ui.label(if status.status.len() > STATUS_LENGTH - 3 {
format!("{}...", &status.status[0..STATUS_LENGTH - 3])
} else {
format!("{:width$}", &status.status, width = STATUS_LENGTH)
});
}
});
}
}
}

4
src/views/mod.rs Normal file
View File

@@ -0,0 +1,4 @@
pub(crate) mod config;
pub(crate) mod function_diff;
pub(crate) mod jobs;
pub(crate) mod symbol_diff;

240
src/views/symbol_diff.rs Normal file
View File

@@ -0,0 +1,240 @@
use egui::{
text::LayoutJob, CollapsingHeader, Color32, FontFamily, FontId, Rgba, ScrollArea,
SelectableLabel, TextFormat, Ui, Widget,
};
use egui_extras::{Size, StripBuilder};
use crate::{
app::{View, ViewState},
jobs::build::BuildStatus,
obj::{ObjInfo, ObjSymbol, ObjSymbolFlags},
};
pub fn match_color_for_symbol(symbol: &ObjSymbol) -> Color32 {
if symbol.match_percent == 100.0 {
Color32::GREEN
} else if symbol.match_percent >= 50.0 {
Color32::LIGHT_BLUE
} else {
Color32::RED
}
}
fn symbol_ui(
ui: &mut Ui,
symbol: &ObjSymbol,
highlighted_symbol: &mut Option<String>,
selected_symbol: &mut Option<String>,
current_view: &mut View,
) {
let mut job = LayoutJob::default();
let name: &str =
if let Some(demangled) = &symbol.demangled_name { demangled } else { &symbol.name };
let mut selected = false;
if let Some(sym) = highlighted_symbol {
selected = sym == &symbol.name;
}
let font_id = FontId::new(14.0, FontFamily::Monospace);
job.append("[", 0.0, TextFormat {
font_id: font_id.clone(),
color: Color32::GRAY,
..Default::default()
});
if symbol.flags.0.contains(ObjSymbolFlags::Common) {
job.append("c", 0.0, TextFormat {
font_id: font_id.clone(),
color: Color32::from_rgb(0, 255, 255),
..Default::default()
});
} else if symbol.flags.0.contains(ObjSymbolFlags::Global) {
job.append("g", 0.0, TextFormat {
font_id: font_id.clone(),
color: Color32::GREEN,
..Default::default()
});
} else if symbol.flags.0.contains(ObjSymbolFlags::Local) {
job.append("l", 0.0, TextFormat {
font_id: font_id.clone(),
color: Color32::GRAY,
..Default::default()
});
}
if symbol.flags.0.contains(ObjSymbolFlags::Weak) {
job.append("w", 0.0, TextFormat {
font_id: font_id.clone(),
color: Color32::GRAY,
..Default::default()
});
}
job.append("] (", 0.0, TextFormat {
font_id: font_id.clone(),
color: Color32::GRAY,
..Default::default()
});
job.append(&format!("{:.0}%", symbol.match_percent), 0.0, TextFormat {
font_id: font_id.clone(),
color: match_color_for_symbol(symbol),
..Default::default()
});
job.append(") ", 0.0, TextFormat {
font_id: font_id.clone(),
color: Color32::GRAY,
..Default::default()
});
job.append(name, 0.0, TextFormat { font_id, color: Color32::WHITE, ..Default::default() });
let response = SelectableLabel::new(selected, job).ui(ui);
if response.clicked() {
*selected_symbol = Some(symbol.name.clone());
*current_view = View::FunctionDiff;
} else if response.hovered() {
*highlighted_symbol = Some(symbol.name.clone());
}
}
fn symbol_list_ui(
ui: &mut Ui,
obj: &ObjInfo,
highlighted_symbol: &mut Option<String>,
selected_symbol: &mut Option<String>,
current_view: &mut View,
reverse_function_order: bool,
) {
ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| {
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false);
if !obj.common.is_empty() {
CollapsingHeader::new(".comm").default_open(true).show(ui, |ui| {
for symbol in &obj.common {
symbol_ui(ui, symbol, highlighted_symbol, selected_symbol, current_view);
}
});
}
for section in &obj.sections {
CollapsingHeader::new(format!("{} ({:x})", section.name, section.size))
.default_open(true)
.show(ui, |ui| {
if section.name == ".text" && reverse_function_order {
for symbol in section.symbols.iter().rev() {
symbol_ui(
ui,
symbol,
highlighted_symbol,
selected_symbol,
current_view,
);
}
} else {
for symbol in &section.symbols {
symbol_ui(
ui,
symbol,
highlighted_symbol,
selected_symbol,
current_view,
);
}
}
});
}
});
});
}
fn build_log_ui(ui: &mut Ui, status: &BuildStatus) {
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false);
ui.colored_label(Color32::from_rgb(255, 0, 0), &status.log);
});
}
pub fn symbol_diff_ui(ui: &mut Ui, view_state: &mut ViewState) {
if let (Some(result), highlighted_symbol, selected_symbol, current_view) = (
&view_state.build,
&mut view_state.highlighted_symbol,
&mut view_state.selected_symbol,
&mut view_state.current_view,
) {
StripBuilder::new(ui).size(Size::exact(40.0)).size(Size::remainder()).vertical(
|mut strip| {
strip.strip(|builder| {
builder.sizes(Size::remainder(), 2).horizontal(|mut strip| {
strip.cell(|ui| {
ui.scope(|ui| {
ui.style_mut().override_text_style =
Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false);
ui.label("Build asm:");
if result.first_status.success {
ui.label("OK");
} else {
ui.colored_label(Rgba::from_rgb(1.0, 0.0, 0.0), "Fail");
}
});
ui.separator();
});
strip.cell(|ui| {
ui.scope(|ui| {
ui.style_mut().override_text_style =
Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false);
ui.label("Build src:");
if result.second_status.success {
ui.label("OK");
} else {
ui.colored_label(Rgba::from_rgb(1.0, 0.0, 0.0), "Fail");
}
});
ui.separator();
});
});
});
strip.strip(|builder| {
builder.sizes(Size::remainder(), 2).horizontal(|mut strip| {
strip.cell(|ui| {
if result.first_status.success {
if let Some(obj) = &result.first_obj {
ui.push_id("left", |ui| {
symbol_list_ui(
ui,
obj,
highlighted_symbol,
selected_symbol,
current_view,
view_state.reverse_fn_order,
);
});
}
} else {
build_log_ui(ui, &result.first_status);
}
});
strip.cell(|ui| {
if result.second_status.success {
if let Some(obj) = &result.second_obj {
ui.push_id("right", |ui| {
symbol_list_ui(
ui,
obj,
highlighted_symbol,
selected_symbol,
current_view,
view_state.reverse_fn_order,
);
});
}
} else {
build_log_ui(ui, &result.second_status);
}
});
});
});
},
);
}
}