Compare commits

..

5 Commits

Author SHA1 Message Date
LagoLunatic
221c1a8cda Merge 0b7afa9f1e into 7aa878b48e 2024-12-01 22:33:58 -07:00
LagoLunatic
0b7afa9f1e Remove trailing newline when displaying decoded rlwinm info 2024-12-01 20:34:48 -05:00
LagoLunatic
5841d4db8a Display decoded rlwinm info to hover tooltip 2024-12-01 20:27:56 -05:00
LagoLunatic
7a360d4a99 Update .gitignore 2024-12-01 20:26:17 -05:00
LagoLunatic
40c97a1bc3 Fix missing dependency feature for objdiff-gui 2024-12-01 20:26:17 -05:00
58 changed files with 2454 additions and 4643 deletions

View File

@@ -65,7 +65,7 @@ jobs:
continue-on-error: ${{ matrix.checks == 'advisories' }}
steps:
- uses: actions/checkout@v4
- uses: EmbarkStudios/cargo-deny-action@v2
- uses: EmbarkStudios/cargo-deny-action@v1
with:
command: check ${{ matrix.checks }}

1033
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -13,9 +13,9 @@ strip = "debuginfo"
codegen-units = 1
[workspace.package]
version = "2.7.1"
version = "2.4.0"
authors = ["Luke Street <luke@street.dev>"]
edition = "2021"
license = "MIT OR Apache-2.0"
repository = "https://github.com/encounter/objdiff"
rust-version = "1.81"
rust-version = "1.74"

View File

@@ -73,6 +73,7 @@ ignore = [
#{ id = "RUSTSEC-0000-0000", reason = "you can specify a reason the advisory is ignored" },
#"a-crate-that-is-yanked@0.1.1", # you can also ignore yanked crate versions if you wish
#{ crate = "a-crate-that-is-yanked@0.1.1", reason = "you can specify why you are ignoring the yanked crate" },
{ id = "RUSTSEC-2024-0384", reason = "Unmaintained indirect dependency" },
]
# If this is true, then cargo deny will use the git executable to fetch advisory database.
# If this is false, then it uses a built-in git library.
@@ -239,7 +240,7 @@ allow-git = []
[sources.allow-org]
# github.com organizations to allow git sources for
github = []
github = ["notify-rs"]
# gitlab.com organizations to allow git sources for
gitlab = []
# bitbucket.org organizations to allow git sources for

View File

@@ -14,7 +14,7 @@ publish = false
[dependencies]
anyhow = "1.0"
argp = "0.4"
argp = "0.3"
crossterm = "0.28"
enable-ansi-support = "0.2"
memmap2 = "0.9"

File diff suppressed because it is too large Load Diff

View File

@@ -169,29 +169,22 @@ fn report_object(
}
_ => {}
}
let diff_config = diff::DiffObjConfig {
function_reloc_diffs: diff::FunctionRelocDiffs::None,
..Default::default()
};
let mapping_config = diff::MappingConfig::default();
let config = diff::DiffObjConfig { relax_reloc_diffs: true, ..Default::default() };
let target = object
.target_path
.as_ref()
.map(|p| {
obj::read::read(p, &diff_config)
.with_context(|| format!("Failed to open {}", p.display()))
obj::read::read(p, &config).with_context(|| format!("Failed to open {}", p.display()))
})
.transpose()?;
let base = object
.base_path
.as_ref()
.map(|p| {
obj::read::read(p, &diff_config)
.with_context(|| format!("Failed to open {}", p.display()))
obj::read::read(p, &config).with_context(|| format!("Failed to open {}", p.display()))
})
.transpose()?;
let result =
diff::diff_objs(&diff_config, &mapping_config, target.as_ref(), base.as_ref(), None)?;
let result = diff::diff_objs(&config, target.as_ref(), base.as_ref(), None)?;
let metadata = ReportUnitMetadata {
complete: object.complete(),

View File

@@ -1,7 +1,6 @@
mod argp_version;
mod cmd;
mod util;
mod views;
// musl's allocator is very slow, so use mimalloc when targeting musl.
// Otherwise, use the system allocator to avoid extra code size.

View File

@@ -1,658 +0,0 @@
use anyhow::{bail, Result};
use crossterm::event::{Event, KeyCode, KeyEventKind, KeyModifiers, MouseButton, MouseEventKind};
use objdiff_core::{
diff::{
display::{display_diff, DiffText, HighlightKind},
FunctionRelocDiffs, ObjDiff, ObjInsDiffKind, ObjSymbolDiff,
},
obj::{ObjInfo, ObjSectionKind, ObjSymbol, SymbolRef},
};
use ratatui::{
prelude::*,
widgets::{Block, Borders, Clear, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState},
Frame,
};
use super::{EventControlFlow, EventResult, UiView};
use crate::cmd::diff::AppState;
#[allow(dead_code)]
#[derive(Default)]
pub struct FunctionDiffUi {
pub symbol_name: String,
pub left_highlight: HighlightKind,
pub right_highlight: HighlightKind,
pub scroll_x: usize,
pub scroll_state_x: ScrollbarState,
pub scroll_y: usize,
pub scroll_state_y: ScrollbarState,
pub per_page: usize,
pub num_rows: usize,
pub left_sym: Option<SymbolRef>,
pub right_sym: Option<SymbolRef>,
pub prev_sym: Option<SymbolRef>,
pub open_options: bool,
pub three_way: bool,
}
impl UiView for FunctionDiffUi {
fn draw(&mut self, state: &AppState, f: &mut Frame, result: &mut EventResult) {
let chunks = Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]).split(f.area());
let header_chunks = Layout::horizontal([
Constraint::Fill(1),
Constraint::Length(3),
Constraint::Fill(1),
Constraint::Length(2),
])
.split(chunks[0]);
let content_chunks = if self.three_way {
Layout::horizontal([
Constraint::Fill(1),
Constraint::Length(3),
Constraint::Fill(1),
Constraint::Length(3),
Constraint::Fill(1),
Constraint::Length(2),
])
.split(chunks[1])
} else {
Layout::horizontal([
Constraint::Fill(1),
Constraint::Length(3),
Constraint::Fill(1),
Constraint::Length(2),
])
.split(chunks[1])
};
self.per_page = chunks[1].height.saturating_sub(2) as usize;
let max_scroll_y = self.num_rows.saturating_sub(self.per_page);
if self.scroll_y > max_scroll_y {
self.scroll_y = max_scroll_y;
}
self.scroll_state_y =
self.scroll_state_y.content_length(max_scroll_y).position(self.scroll_y);
let mut line_l = Line::default();
line_l
.spans
.push(Span::styled(self.symbol_name.clone(), Style::new().fg(Color::White).bold()));
f.render_widget(line_l, header_chunks[0]);
let mut line_r = Line::default();
if let Some(percent) =
get_symbol(state.right_obj.as_ref(), self.right_sym).and_then(|(_, d)| d.match_percent)
{
line_r.spans.push(Span::styled(
format!("{:.2}% ", percent),
Style::new().fg(match_percent_color(percent)),
));
}
let reload_time = state
.reload_time
.as_ref()
.and_then(|t| t.format(&state.time_format).ok())
.unwrap_or_else(|| "N/A".to_string());
line_r.spans.push(Span::styled(
format!("Last reload: {}", reload_time),
Style::new().fg(Color::White),
));
line_r.spans.push(Span::styled(
format!(" ({} jobs)", state.jobs.jobs.len()),
Style::new().fg(Color::LightYellow),
));
f.render_widget(line_r, header_chunks[2]);
let mut left_text = None;
let mut left_highlight = None;
let mut max_width = 0;
if let Some((symbol, symbol_diff)) = get_symbol(state.left_obj.as_ref(), self.left_sym) {
let mut text = Text::default();
let rect = content_chunks[0].inner(Margin::new(0, 1));
left_highlight = self.print_sym(
&mut text,
symbol,
symbol_diff,
rect,
&self.left_highlight,
result,
false,
);
max_width = max_width.max(text.width());
left_text = Some(text);
}
let mut right_text = None;
let mut right_highlight = None;
let mut margin_text = None;
if let Some((symbol, symbol_diff)) = get_symbol(state.right_obj.as_ref(), self.right_sym) {
let mut text = Text::default();
let rect = content_chunks[2].inner(Margin::new(0, 1));
right_highlight = self.print_sym(
&mut text,
symbol,
symbol_diff,
rect,
&self.right_highlight,
result,
false,
);
max_width = max_width.max(text.width());
right_text = Some(text);
// Render margin
let mut text = Text::default();
let rect = content_chunks[1].inner(Margin::new(1, 1));
self.print_margin(&mut text, symbol_diff, rect);
margin_text = Some(text);
}
let mut prev_text = None;
let mut prev_margin_text = None;
if self.three_way {
if let Some((symbol, symbol_diff)) = get_symbol(state.prev_obj.as_ref(), self.prev_sym)
{
let mut text = Text::default();
let rect = content_chunks[4].inner(Margin::new(0, 1));
self.print_sym(
&mut text,
symbol,
symbol_diff,
rect,
&self.right_highlight,
result,
true,
);
max_width = max_width.max(text.width());
prev_text = Some(text);
// Render margin
let mut text = Text::default();
let rect = content_chunks[3].inner(Margin::new(1, 1));
self.print_margin(&mut text, symbol_diff, rect);
prev_margin_text = Some(text);
}
}
let max_scroll_x =
max_width.saturating_sub(content_chunks[0].width.min(content_chunks[2].width) as usize);
if self.scroll_x > max_scroll_x {
self.scroll_x = max_scroll_x;
}
self.scroll_state_x =
self.scroll_state_x.content_length(max_scroll_x).position(self.scroll_x);
if let Some(text) = left_text {
// Render left column
f.render_widget(
Paragraph::new(text)
.block(
Block::new()
.borders(Borders::TOP)
.border_style(Style::new().fg(Color::Gray))
.title_style(Style::new().bold())
.title("TARGET"),
)
.scroll((0, self.scroll_x as u16)),
content_chunks[0],
);
}
if let Some(text) = margin_text {
f.render_widget(text, content_chunks[1].inner(Margin::new(1, 1)));
}
if let Some(text) = right_text {
f.render_widget(
Paragraph::new(text)
.block(
Block::new()
.borders(Borders::TOP)
.border_style(Style::new().fg(Color::Gray))
.title_style(Style::new().bold())
.title("CURRENT"),
)
.scroll((0, self.scroll_x as u16)),
content_chunks[2],
);
}
if self.three_way {
if let Some(text) = prev_margin_text {
f.render_widget(text, content_chunks[3].inner(Margin::new(1, 1)));
}
let block = Block::new()
.borders(Borders::TOP)
.border_style(Style::new().fg(Color::Gray))
.title_style(Style::new().bold())
.title("SAVED");
if let Some(text) = prev_text {
f.render_widget(
Paragraph::new(text).block(block.clone()).scroll((0, self.scroll_x as u16)),
content_chunks[4],
);
} else {
f.render_widget(block, content_chunks[4]);
}
}
// Render scrollbars
f.render_stateful_widget(
Scrollbar::new(ScrollbarOrientation::VerticalRight).begin_symbol(None).end_symbol(None),
chunks[1].inner(Margin::new(0, 1)),
&mut self.scroll_state_y,
);
f.render_stateful_widget(
Scrollbar::new(ScrollbarOrientation::HorizontalBottom).thumb_symbol(""),
content_chunks[0],
&mut self.scroll_state_x,
);
f.render_stateful_widget(
Scrollbar::new(ScrollbarOrientation::HorizontalBottom).thumb_symbol(""),
content_chunks[2],
&mut self.scroll_state_x,
);
if self.three_way {
f.render_stateful_widget(
Scrollbar::new(ScrollbarOrientation::HorizontalBottom).thumb_symbol(""),
content_chunks[4],
&mut self.scroll_state_x,
);
}
if let Some(new_highlight) = left_highlight {
if new_highlight == self.left_highlight {
if self.left_highlight != self.right_highlight {
self.right_highlight = self.left_highlight.clone();
} else {
self.left_highlight = HighlightKind::None;
self.right_highlight = HighlightKind::None;
}
} else {
self.left_highlight = new_highlight;
}
result.redraw = true;
} else if let Some(new_highlight) = right_highlight {
if new_highlight == self.right_highlight {
if self.left_highlight != self.right_highlight {
self.left_highlight = self.right_highlight.clone();
} else {
self.left_highlight = HighlightKind::None;
self.right_highlight = HighlightKind::None;
}
} else {
self.right_highlight = new_highlight;
}
result.redraw = true;
}
if self.open_options {
self.draw_options(f, result);
}
}
fn handle_event(&mut self, state: &mut AppState, event: Event) -> EventControlFlow {
let mut result = EventResult::default();
match event {
Event::Key(event)
if matches!(event.kind, KeyEventKind::Press | KeyEventKind::Repeat) =>
{
match event.code {
// Quit
KeyCode::Esc | KeyCode::Char('q') => return EventControlFlow::Break,
// Page up
KeyCode::PageUp => {
self.page_up(false);
result.redraw = true;
}
// Page up (shift + space)
KeyCode::Char(' ') if event.modifiers.contains(KeyModifiers::SHIFT) => {
self.page_up(false);
result.redraw = true;
}
// Page down
KeyCode::Char(' ') | KeyCode::PageDown => {
self.page_down(false);
result.redraw = true;
}
// Page down (ctrl + f)
KeyCode::Char('f') if event.modifiers.contains(KeyModifiers::CONTROL) => {
self.page_down(false);
result.redraw = true;
}
// Page up (ctrl + b)
KeyCode::Char('b') if event.modifiers.contains(KeyModifiers::CONTROL) => {
self.page_up(false);
result.redraw = true;
}
// Half page down (ctrl + d)
KeyCode::Char('d') if event.modifiers.contains(KeyModifiers::CONTROL) => {
self.page_down(true);
result.redraw = true;
}
// Half page up (ctrl + u)
KeyCode::Char('u') if event.modifiers.contains(KeyModifiers::CONTROL) => {
self.page_up(true);
result.redraw = true;
}
// Scroll down
KeyCode::Down | KeyCode::Char('j') => {
self.scroll_y += 1;
result.redraw = true;
}
// Scroll up
KeyCode::Up | KeyCode::Char('k') => {
self.scroll_y = self.scroll_y.saturating_sub(1);
result.redraw = true;
}
// Scroll to start
KeyCode::Char('g') => {
self.scroll_y = 0;
result.redraw = true;
}
// Scroll to end
KeyCode::Char('G') => {
self.scroll_y = self.num_rows;
result.redraw = true;
}
// Reload
KeyCode::Char('r') => {
result.redraw = true;
return EventControlFlow::Reload;
}
// Scroll right
KeyCode::Right | KeyCode::Char('l') => {
self.scroll_x += 1;
result.redraw = true;
}
// Scroll left
KeyCode::Left | KeyCode::Char('h') => {
self.scroll_x = self.scroll_x.saturating_sub(1);
result.redraw = true;
}
// Cycle through function relocation diff mode
KeyCode::Char('x') => {
state.diff_obj_config.function_reloc_diffs =
match state.diff_obj_config.function_reloc_diffs {
FunctionRelocDiffs::None => FunctionRelocDiffs::NameAddress,
FunctionRelocDiffs::NameAddress => FunctionRelocDiffs::DataValue,
FunctionRelocDiffs::DataValue => FunctionRelocDiffs::All,
FunctionRelocDiffs::All => FunctionRelocDiffs::None,
};
result.redraw = true;
return EventControlFlow::Reload;
}
// Toggle three-way diff
KeyCode::Char('3') => {
self.three_way = !self.three_way;
result.redraw = true;
}
// Toggle options
KeyCode::Char('o') => {
self.open_options = !self.open_options;
result.redraw = true;
}
_ => {}
}
}
Event::Mouse(event) => match event.kind {
MouseEventKind::ScrollDown => {
self.scroll_y += 3;
result.redraw = true;
}
MouseEventKind::ScrollUp => {
self.scroll_y = self.scroll_y.saturating_sub(3);
result.redraw = true;
}
MouseEventKind::ScrollRight => {
self.scroll_x += 3;
result.redraw = true;
}
MouseEventKind::ScrollLeft => {
self.scroll_x = self.scroll_x.saturating_sub(3);
result.redraw = true;
}
MouseEventKind::Down(MouseButton::Left) => {
result.click_xy = Some((event.column, event.row));
result.redraw = true;
}
_ => {}
},
Event::Resize(_, _) => {
result.redraw = true;
}
_ => {}
}
EventControlFlow::Continue(result)
}
fn reload(&mut self, state: &AppState) -> Result<()> {
let left_sym =
state.left_obj.as_ref().and_then(|(o, _)| find_function(o, &self.symbol_name));
let right_sym =
state.right_obj.as_ref().and_then(|(o, _)| find_function(o, &self.symbol_name));
let prev_sym =
state.prev_obj.as_ref().and_then(|(o, _)| find_function(o, &self.symbol_name));
self.num_rows = match (
get_symbol(state.left_obj.as_ref(), left_sym),
get_symbol(state.right_obj.as_ref(), right_sym),
) {
(Some((_l, ld)), Some((_r, rd))) => ld.instructions.len().max(rd.instructions.len()),
(Some((_l, ld)), None) => ld.instructions.len(),
(None, Some((_r, rd))) => rd.instructions.len(),
(None, None) => bail!("Symbol not found: {}", self.symbol_name),
};
self.left_sym = left_sym;
self.right_sym = right_sym;
self.prev_sym = prev_sym;
Ok(())
}
}
impl FunctionDiffUi {
pub fn draw_options(&mut self, f: &mut Frame, _result: &mut EventResult) {
let percent_x = 50;
let percent_y = 50;
let popup_rect = Layout::vertical([
Constraint::Percentage((100 - percent_y) / 2),
Constraint::Percentage(percent_y),
Constraint::Percentage((100 - percent_y) / 2),
])
.split(f.area())[1];
let popup_rect = Layout::horizontal([
Constraint::Percentage((100 - percent_x) / 2),
Constraint::Percentage(percent_x),
Constraint::Percentage((100 - percent_x) / 2),
])
.split(popup_rect)[1];
let popup = Block::default()
.borders(Borders::ALL)
.title("Options")
.title_style(Style::default().fg(Color::White).bg(Color::Black));
f.render_widget(Clear, popup_rect);
f.render_widget(popup, popup_rect);
}
fn page_up(&mut self, half: bool) {
self.scroll_y = self.scroll_y.saturating_sub(self.per_page / if half { 2 } else { 1 });
}
fn page_down(&mut self, half: bool) {
self.scroll_y += self.per_page / if half { 2 } else { 1 };
}
#[allow(clippy::too_many_arguments)]
fn print_sym(
&self,
out: &mut Text<'static>,
symbol: &ObjSymbol,
symbol_diff: &ObjSymbolDiff,
rect: Rect,
highlight: &HighlightKind,
result: &EventResult,
only_changed: bool,
) -> Option<HighlightKind> {
let base_addr = symbol.address;
let mut new_highlight = None;
for (y, ins_diff) in symbol_diff
.instructions
.iter()
.skip(self.scroll_y)
.take(rect.height as usize)
.enumerate()
{
if only_changed && ins_diff.kind == ObjInsDiffKind::None {
out.lines.push(Line::default());
continue;
}
let mut sx = rect.x;
let sy = rect.y + y as u16;
let mut line = Line::default();
display_diff(ins_diff, base_addr, |text| -> Result<()> {
let label_text;
let mut base_color = match ins_diff.kind {
ObjInsDiffKind::None
| ObjInsDiffKind::OpMismatch
| ObjInsDiffKind::ArgMismatch => Color::Gray,
ObjInsDiffKind::Replace => Color::Cyan,
ObjInsDiffKind::Delete => Color::Red,
ObjInsDiffKind::Insert => Color::Green,
};
let mut pad_to = 0;
match text {
DiffText::Basic(text) => {
label_text = text.to_string();
}
DiffText::BasicColor(s, idx) => {
label_text = s.to_string();
base_color = COLOR_ROTATION[idx % COLOR_ROTATION.len()];
}
DiffText::Line(num) => {
label_text = format!("{num} ");
base_color = Color::DarkGray;
pad_to = 5;
}
DiffText::Address(addr) => {
label_text = format!("{:x}:", addr);
pad_to = 5;
}
DiffText::Opcode(mnemonic, _op) => {
label_text = mnemonic.to_string();
if ins_diff.kind == ObjInsDiffKind::OpMismatch {
base_color = Color::Blue;
}
pad_to = 8;
}
DiffText::Argument(arg, diff) => {
label_text = arg.to_string();
if let Some(diff) = diff {
base_color = COLOR_ROTATION[diff.idx % COLOR_ROTATION.len()]
}
}
DiffText::BranchDest(addr, diff) => {
label_text = format!("{addr:x}");
if let Some(diff) = diff {
base_color = COLOR_ROTATION[diff.idx % COLOR_ROTATION.len()]
}
}
DiffText::Symbol(sym, diff) => {
let name = sym.demangled_name.as_ref().unwrap_or(&sym.name);
label_text = name.clone();
if let Some(diff) = diff {
base_color = COLOR_ROTATION[diff.idx % COLOR_ROTATION.len()]
} else {
base_color = Color::White;
}
}
DiffText::Spacing(n) => {
line.spans.push(Span::raw(" ".repeat(n)));
sx += n as u16;
return Ok(());
}
DiffText::Eol => {
return Ok(());
}
}
let len = label_text.len();
let highlighted = *highlight == text;
if let Some((cx, cy)) = result.click_xy {
if cx >= sx && cx < sx + len as u16 && cy == sy {
new_highlight = Some(text.into());
}
}
let mut style = Style::new().fg(base_color);
if highlighted {
style = style.bg(Color::DarkGray);
}
line.spans.push(Span::styled(label_text, style));
sx += len as u16;
if pad_to > len {
let pad = (pad_to - len) as u16;
line.spans.push(Span::raw(" ".repeat(pad as usize)));
sx += pad;
}
Ok(())
})
.unwrap();
out.lines.push(line);
}
new_highlight
}
fn print_margin(&self, out: &mut Text, symbol: &ObjSymbolDiff, rect: Rect) {
for ins_diff in symbol.instructions.iter().skip(self.scroll_y).take(rect.height as usize) {
if ins_diff.kind != ObjInsDiffKind::None {
out.lines.push(Line::raw(match ins_diff.kind {
ObjInsDiffKind::Delete => "<",
ObjInsDiffKind::Insert => ">",
_ => "|",
}));
} else {
out.lines.push(Line::raw(" "));
}
}
}
}
pub const COLOR_ROTATION: [Color; 7] = [
Color::Magenta,
Color::Cyan,
Color::Green,
Color::Red,
Color::Yellow,
Color::Blue,
Color::Green,
];
pub fn match_percent_color(match_percent: f32) -> Color {
if match_percent == 100.0 {
Color::Green
} else if match_percent >= 50.0 {
Color::LightBlue
} else {
Color::LightRed
}
}
#[inline]
fn get_symbol(
obj: Option<&(ObjInfo, ObjDiff)>,
sym: Option<SymbolRef>,
) -> Option<(&ObjSymbol, &ObjSymbolDiff)> {
let (obj, diff) = obj?;
let sym = sym?;
Some((obj.section_symbol(sym).1, diff.symbol_diff(sym)))
}
fn find_function(obj: &ObjInfo, name: &str) -> Option<SymbolRef> {
for (section_idx, section) in obj.sections.iter().enumerate() {
if section.kind != ObjSectionKind::Code {
continue;
}
for (symbol_idx, symbol) in section.symbols.iter().enumerate() {
if symbol.name == name {
return Some(SymbolRef { section_idx, symbol_idx });
}
}
}
None
}

View File

@@ -1,25 +0,0 @@
use anyhow::Result;
use crossterm::event::Event;
use ratatui::Frame;
use crate::cmd::diff::AppState;
pub mod function_diff;
#[derive(Default)]
pub struct EventResult {
pub redraw: bool,
pub click_xy: Option<(u16, u16)>,
}
pub enum EventControlFlow {
Break,
Continue(EventResult),
Reload,
}
pub trait UiView {
fn draw(&mut self, state: &AppState, f: &mut Frame, result: &mut EventResult);
fn handle_event(&mut self, state: &mut AppState, event: Event) -> EventControlFlow;
fn reload(&mut self, state: &AppState) -> Result<()>;
}

View File

@@ -16,104 +16,17 @@ documentation = "https://docs.rs/objdiff-core"
crate-type = ["cdylib", "rlib"]
[features]
all = [
# Features
"bindings",
"build",
"config",
"dwarf",
# Architectures
"mips",
"ppc",
"x86",
"arm",
"arm64",
]
# Implicit, used to check if any arch is enabled
any-arch = [
"config",
"dep:bimap",
"dep:byteorder",
"dep:flagset",
"dep:heck",
"dep:log",
"dep:memmap2",
"dep:num-traits",
"dep:prettyplease",
"dep:proc-macro2",
"dep:quote",
"dep:serde",
"dep:serde_json",
"dep:similar",
"dep:strum",
"dep:syn",
]
bindings = [
"dep:pbjson",
"dep:pbjson-build",
"dep:prost",
"dep:prost-build",
"dep:serde",
"dep:serde_json",
]
build = [
"dep:notify",
"dep:notify-debouncer-full",
"dep:path-slash",
"dep:reqwest",
"dep:self_update",
"dep:shell-escape",
"dep:tempfile",
"dep:time",
"dep:winapi",
]
config = [
"dep:bimap",
"dep:filetime",
"dep:globset",
"dep:semver",
"dep:serde",
"dep:serde_json",
"dep:serde_yaml",
]
all = ["config", "dwarf", "mips", "ppc", "x86", "arm", "arm64", "bindings"]
any-arch = ["config", "dep:bimap", "dep:strum", "dep:similar", "dep:flagset", "dep:log", "dep:memmap2", "dep:byteorder", "dep:num-traits"] # Implicit, used to check if any arch is enabled
config = ["dep:bimap", "dep:globset", "dep:semver", "dep:serde_json", "dep:serde_yaml", "dep:serde", "dep:filetime"]
dwarf = ["dep:gimli"]
mips = [
"any-arch",
"dep:rabbitizer",
]
ppc = [
"any-arch",
"dep:cwdemangle",
"dep:cwextab",
"dep:ppc750cl",
]
x86 = [
"any-arch",
"dep:cpp_demangle",
"dep:iced-x86",
"dep:msvc-demangler",
]
arm = [
"any-arch",
"dep:arm-attr",
"dep:cpp_demangle",
"dep:unarm",
]
arm64 = [
"any-arch",
"dep:cpp_demangle",
"dep:yaxpeax-arch",
"dep:yaxpeax-arm",
]
wasm = [
"any-arch",
"bindings",
"dep:console_error_panic_hook",
"dep:console_log",
"dep:log",
"dep:tsify-next",
"dep:wasm-bindgen",
]
mips = ["any-arch", "dep:rabbitizer"]
ppc = ["any-arch", "dep:cwdemangle", "dep:cwextab", "dep:ppc750cl"]
x86 = ["any-arch", "dep:cpp_demangle", "dep:iced-x86", "dep:msvc-demangler"]
arm = ["any-arch", "dep:cpp_demangle", "dep:unarm", "dep:arm-attr"]
arm64 = ["any-arch", "dep:cpp_demangle", "dep:yaxpeax-arch", "dep:yaxpeax-arm"]
bindings = ["dep:serde_json", "dep:prost", "dep:pbjson", "dep:serde", "dep:prost-build", "dep:pbjson-build"]
wasm = ["bindings", "any-arch", "dep:console_error_panic_hook", "dep:console_log", "dep:wasm-bindgen", "dep:tsify-next", "dep:log"]
[package.metadata.docs.rs]
features = ["all"]
@@ -149,7 +62,7 @@ gimli = { version = "0.31", default-features = false, features = ["read-all"], o
# ppc
cwdemangle = { version = "1.0", optional = true }
cwextab = { version = "1.0", optional = true }
cwextab = { version = "1.0.2", optional = true }
ppc750cl = { version = "0.3", optional = true }
# mips
@@ -168,34 +81,6 @@ arm-attr = { version = "0.1", optional = true }
yaxpeax-arch = { version = "0.3", default-features = false, features = ["std"], optional = true }
yaxpeax-arm = { version = "0.3", default-features = false, features = ["std"], optional = true }
# build
notify = { version = "8.0.0", optional = true }
notify-debouncer-full = { version = "0.5.0", optional = true }
shell-escape = { version = "0.1", optional = true }
tempfile = { version = "3.15", optional = true }
time = { version = "0.3", optional = true }
[target.'cfg(windows)'.dependencies]
path-slash = { version = "0.2", optional = true }
winapi = { version = "0.3", optional = true }
# For Linux static binaries, use rustls
[target.'cfg(target_os = "linux")'.dependencies]
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "multipart", "rustls-tls"], optional = true }
self_update = { version = "0.42", default-features = false, features = ["rustls"], optional = true }
# For all other platforms, use native TLS
[target.'cfg(not(target_os = "linux"))'.dependencies]
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "multipart", "default-tls"], optional = true }
self_update = { version = "0.42", optional = true }
[build-dependencies]
heck = { version = "0.5", optional = true }
pbjson-build = { version = "0.7", optional = true }
prettyplease = { version = "0.2", optional = true }
proc-macro2 = { version = "1.0", optional = true }
prost-build = { version = "0.13", optional = true }
quote = { version = "1.0", optional = true }
serde = { version = "1.0", features = ["derive"], optional = true }
serde_json = { version = "1.0", optional = true }
syn = { version = "2.0", optional = true }
pbjson-build = { version = "0.7", optional = true }

View File

@@ -1,11 +1,6 @@
#[cfg(feature = "any-arch")]
mod config_gen;
fn main() {
#[cfg(feature = "bindings")]
compile_protos();
#[cfg(feature = "any-arch")]
config_gen::generate_diff_config();
}
#[cfg(feature = "bindings")]

View File

@@ -1,248 +0,0 @@
{
"properties": [
{
"id": "functionRelocDiffs",
"type": "choice",
"default": "name_address",
"name": "Function relocation diffs",
"description": "How relocation targets will be diffed in the function view.",
"items": [
{
"value": "none",
"name": "None"
},
{
"value": "name_address",
"name": "Name or address"
},
{
"value": "data_value",
"name": "Data value"
},
{
"value": "all",
"name": "Name or address, data value"
}
]
},
{
"id": "spaceBetweenArgs",
"type": "boolean",
"default": true,
"name": "Space between args",
"description": "Adds a space between arguments in the diff output."
},
{
"id": "combineDataSections",
"type": "boolean",
"default": false,
"name": "Combine data sections",
"description": "Combines data sections with equal names."
},
{
"id": "arm.archVersion",
"type": "choice",
"default": "auto",
"name": "Architecture version",
"description": "ARM architecture version to use for disassembly.",
"items": [
{
"value": "auto",
"name": "Auto"
},
{
"value": "v4t",
"name": "ARMv4T (GBA)"
},
{
"value": "v5te",
"name": "ARMv5TE (DS)"
},
{
"value": "v6k",
"name": "ARMv6K (3DS)"
}
]
},
{
"id": "arm.unifiedSyntax",
"type": "boolean",
"default": false,
"name": "Unified syntax",
"description": "Disassemble as unified assembly language (UAL)."
},
{
"id": "arm.avRegisters",
"type": "boolean",
"default": false,
"name": "Use A/V registers",
"description": "Display R0-R3 as A1-A4 and R4-R11 as V1-V8."
},
{
"id": "arm.r9Usage",
"type": "choice",
"default": "generalPurpose",
"name": "Display R9 as",
"items": [
{
"value": "generalPurpose",
"name": "R9 or V6",
"description": "Use R9 as a general-purpose register."
},
{
"value": "sb",
"name": "SB (static base)",
"description": "Used for position-independent data (PID)."
},
{
"value": "tr",
"name": "TR (TLS register)",
"description": "Used for thread-local storage."
}
]
},
{
"id": "arm.slUsage",
"type": "boolean",
"default": false,
"name": "Display R10 as SL",
"description": "Used for explicit stack limits."
},
{
"id": "arm.fpUsage",
"type": "boolean",
"default": false,
"name": "Display R11 as FP",
"description": "Used for frame pointers."
},
{
"id": "arm.ipUsage",
"type": "boolean",
"default": false,
"name": "Display R12 as IP",
"description": "Used for interworking and long branches."
},
{
"id": "mips.abi",
"type": "choice",
"default": "auto",
"name": "ABI",
"description": "MIPS ABI to use for disassembly.",
"items": [
{
"value": "auto",
"name": "Auto"
},
{
"value": "o32",
"name": "O32"
},
{
"value": "n32",
"name": "N32"
},
{
"value": "n64",
"name": "N64"
}
]
},
{
"id": "mips.instrCategory",
"type": "choice",
"default": "auto",
"name": "Instruction category",
"description": "MIPS instruction category to use for disassembly.",
"items": [
{
"value": "auto",
"name": "Auto"
},
{
"value": "cpu",
"name": "CPU"
},
{
"value": "rsp",
"name": "RSP (N64)"
},
{
"value": "r3000gte",
"name": "R3000 GTE (PS1)"
},
{
"value": "r4000allegrex",
"name": "R4000 ALLEGREX (PSP)"
},
{
"value": "r5900",
"name": "R5900 EE (PS2)"
}
]
},
{
"id": "x86.formatter",
"type": "choice",
"default": "intel",
"name": "Format",
"description": "x86 disassembly syntax.",
"items": [
{
"value": "intel",
"name": "Intel"
},
{
"value": "gas",
"name": "AT&T"
},
{
"value": "nasm",
"name": "NASM"
},
{
"value": "masm",
"name": "MASM"
}
]
}
],
"groups": [
{
"id": "general",
"name": "General",
"properties": [
"functionRelocDiffs",
"spaceBetweenArgs",
"combineDataSections"
]
},
{
"id": "arm",
"name": "ARM",
"properties": [
"arm.archVersion",
"arm.unifiedSyntax",
"arm.avRegisters",
"arm.r9Usage",
"arm.slUsage",
"arm.fpUsage",
"arm.ipUsage"
]
},
{
"id": "mips",
"name": "MIPS",
"properties": [
"mips.abi",
"mips.instrCategory"
]
},
{
"id": "x86",
"name": "x86",
"properties": [
"x86.formatter"
]
}
]
}

View File

@@ -1,491 +0,0 @@
use std::{
fs::File,
path::{Path, PathBuf},
};
use heck::{ToShoutySnakeCase, ToSnakeCase, ToUpperCamelCase};
use proc_macro2::TokenStream;
use quote::{format_ident, quote};
#[derive(Debug, serde::Deserialize)]
pub struct ConfigSchema {
pub properties: Vec<ConfigProperty>,
pub groups: Vec<ConfigGroup>,
}
#[derive(Debug, serde::Deserialize)]
#[serde(tag = "type")]
pub enum ConfigProperty {
#[serde(rename = "boolean")]
Boolean(ConfigPropertyBoolean),
#[serde(rename = "choice")]
Choice(ConfigPropertyChoice),
}
#[derive(Debug, serde::Deserialize)]
pub struct ConfigPropertyBase {
pub id: String,
pub name: String,
pub description: Option<String>,
}
#[derive(Debug, serde::Deserialize)]
pub struct ConfigPropertyBoolean {
#[serde(flatten)]
pub base: ConfigPropertyBase,
pub default: bool,
}
#[derive(Debug, serde::Deserialize)]
pub struct ConfigPropertyChoice {
#[serde(flatten)]
pub base: ConfigPropertyBase,
pub default: String,
pub items: Vec<ConfigPropertyChoiceItem>,
}
#[derive(Debug, serde::Deserialize)]
pub struct ConfigPropertyChoiceItem {
pub value: String,
pub name: String,
pub description: Option<String>,
}
#[derive(Debug, serde::Deserialize)]
pub struct ConfigGroup {
pub id: String,
pub name: String,
pub description: Option<String>,
pub properties: Vec<String>,
}
fn build_doc(name: &str, description: Option<&str>) -> TokenStream {
let mut doc = format!(" {}", name);
let mut out = quote! { #[doc = #doc] };
if let Some(description) = description {
doc = format!(" {}", description);
out.extend(quote! { #[doc = ""] });
out.extend(quote! { #[doc = #doc] });
}
out
}
pub fn generate_diff_config() {
let schema_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("config-schema.json");
println!("cargo:rerun-if-changed={}", schema_path.display());
let schema_file = File::open(schema_path).expect("Failed to open config schema file");
let schema: ConfigSchema =
serde_json::from_reader(schema_file).expect("Failed to parse config schema");
let mut enums = TokenStream::new();
for property in &schema.properties {
let ConfigProperty::Choice(choice) = property else {
continue;
};
let enum_ident = format_ident!("{}", choice.base.id.to_upper_camel_case());
let mut variants = TokenStream::new();
let mut full_variants = TokenStream::new();
let mut variant_info = TokenStream::new();
let mut variant_to_str = TokenStream::new();
let mut variant_to_name = TokenStream::new();
let mut variant_to_description = TokenStream::new();
let mut variant_from_str = TokenStream::new();
for item in &choice.items {
let variant_name = item.value.to_upper_camel_case();
let variant_ident = format_ident!("{}", variant_name);
let is_default = item.value == choice.default;
variants.extend(build_doc(&item.name, item.description.as_deref()));
if is_default {
variants.extend(quote! { #[default] });
}
let value = &item.value;
variants.extend(quote! {
#[serde(rename = #value, alias = #variant_name)]
#variant_ident,
});
full_variants.extend(quote! { #enum_ident::#variant_ident, });
variant_to_str.extend(quote! { #enum_ident::#variant_ident => #value, });
let name = &item.name;
variant_to_name.extend(quote! { #enum_ident::#variant_ident => #name, });
if let Some(description) = &item.description {
variant_to_description.extend(quote! {
#enum_ident::#variant_ident => Some(#description),
});
} else {
variant_to_description.extend(quote! {
#enum_ident::#variant_ident => None,
});
}
let description = if let Some(description) = &item.description {
quote! { Some(#description) }
} else {
quote! { None }
};
variant_info.extend(quote! {
ConfigEnumVariantInfo {
value: #value,
name: #name,
description: #description,
is_default: #is_default,
},
});
variant_from_str.extend(quote! {
if s.eq_ignore_ascii_case(#value) { return Ok(#enum_ident::#variant_ident); }
});
}
enums.extend(quote! {
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Hash, serde::Deserialize, serde::Serialize)]
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify), tsify(from_wasm_abi))]
pub enum #enum_ident {
#variants
}
impl ConfigEnum for #enum_ident {
#[inline]
fn variants() -> &'static [Self] {
static VARIANTS: &[#enum_ident] = &[#full_variants];
VARIANTS
}
#[inline]
fn variant_info() -> &'static [ConfigEnumVariantInfo] {
static VARIANT_INFO: &[ConfigEnumVariantInfo] = &[
#variant_info
];
VARIANT_INFO
}
fn as_str(&self) -> &'static str {
match self {
#variant_to_str
}
}
fn name(&self) -> &'static str {
match self {
#variant_to_name
}
}
fn description(&self) -> Option<&'static str> {
match self {
#variant_to_description
}
}
}
impl std::str::FromStr for #enum_ident {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
#variant_from_str
Err(())
}
}
});
}
let mut groups = TokenStream::new();
let mut group_idents = Vec::new();
for group in &schema.groups {
let ident = format_ident!("CONFIG_GROUP_{}", group.id.to_shouty_snake_case());
let id = &group.id;
let name = &group.name;
let description = if let Some(description) = &group.description {
quote! { Some(#description) }
} else {
quote! { None }
};
let properties =
group.properties.iter().map(|p| format_ident!("{}", p.to_upper_camel_case()));
groups.extend(quote! {
ConfigPropertyGroup {
id: #id,
name: #name,
description: #description,
properties: &[#(ConfigPropertyId::#properties,)*],
},
});
group_idents.push(ident);
}
let mut property_idents = Vec::new();
let mut property_variants = TokenStream::new();
let mut variant_info = TokenStream::new();
let mut config_property_id_to_str = TokenStream::new();
let mut config_property_id_to_name = TokenStream::new();
let mut config_property_id_to_description = TokenStream::new();
let mut config_property_id_to_kind = TokenStream::new();
let mut property_fields = TokenStream::new();
let mut default_fields = TokenStream::new();
let mut get_property_value_variants = TokenStream::new();
let mut set_property_value_variants = TokenStream::new();
let mut set_property_value_str_variants = TokenStream::new();
let mut config_property_id_from_str = TokenStream::new();
for property in &schema.properties {
let base = match property {
ConfigProperty::Boolean(b) => &b.base,
ConfigProperty::Choice(c) => &c.base,
};
let id = &base.id;
let enum_ident = format_ident!("{}", id.to_upper_camel_case());
property_idents.push(enum_ident.clone());
config_property_id_to_str.extend(quote! { Self::#enum_ident => #id, });
let name = &base.name;
config_property_id_to_name.extend(quote! { Self::#enum_ident => #name, });
if let Some(description) = &base.description {
config_property_id_to_description.extend(quote! {
Self::#enum_ident => Some(#description),
});
} else {
config_property_id_to_description.extend(quote! {
Self::#enum_ident => None,
});
}
let doc = build_doc(name, base.description.as_deref());
property_variants.extend(quote! { #doc #enum_ident, });
property_fields.extend(doc);
let field_ident = format_ident!("{}", id.to_snake_case());
match property {
ConfigProperty::Boolean(b) => {
let default = b.default;
if default {
property_fields.extend(quote! {
#[serde(default = "default_true")]
});
}
property_fields.extend(quote! {
pub #field_ident: bool,
});
default_fields.extend(quote! {
#field_ident: #default,
});
}
ConfigProperty::Choice(_) => {
property_fields.extend(quote! {
pub #field_ident: #enum_ident,
});
default_fields.extend(quote! {
#field_ident: #enum_ident::default(),
});
}
}
let property_value = match property {
ConfigProperty::Boolean(_) => {
quote! { ConfigPropertyValue::Boolean(self.#field_ident) }
}
ConfigProperty::Choice(_) => {
quote! { ConfigPropertyValue::Choice(self.#field_ident.as_str()) }
}
};
get_property_value_variants.extend(quote! {
ConfigPropertyId::#enum_ident => #property_value,
});
match property {
ConfigProperty::Boolean(_) => {
set_property_value_variants.extend(quote! {
ConfigPropertyId::#enum_ident => {
if let ConfigPropertyValue::Boolean(value) = value {
self.#field_ident = value;
Ok(())
} else {
Err(())
}
},
});
set_property_value_str_variants.extend(quote! {
ConfigPropertyId::#enum_ident => {
if let Ok(value) = value.parse() {
self.#field_ident = value;
Ok(())
} else {
Err(())
}
},
});
}
ConfigProperty::Choice(_) => {
set_property_value_variants.extend(quote! {
ConfigPropertyId::#enum_ident => {
if let ConfigPropertyValue::Choice(value) = value {
if let Ok(value) = value.parse() {
self.#field_ident = value;
Ok(())
} else {
Err(())
}
} else {
Err(())
}
},
});
set_property_value_str_variants.extend(quote! {
ConfigPropertyId::#enum_ident => {
if let Ok(value) = value.parse() {
self.#field_ident = value;
Ok(())
} else {
Err(())
}
},
});
}
}
let description = if let Some(description) = &base.description {
quote! { Some(#description) }
} else {
quote! { None }
};
variant_info.extend(quote! {
ConfigEnumVariantInfo {
value: #id,
name: #name,
description: #description,
is_default: false,
},
});
match property {
ConfigProperty::Boolean(_) => {
config_property_id_to_kind.extend(quote! {
Self::#enum_ident => ConfigPropertyKind::Boolean,
});
}
ConfigProperty::Choice(_) => {
config_property_id_to_kind.extend(quote! {
Self::#enum_ident => ConfigPropertyKind::Choice(#enum_ident::variant_info()),
});
}
}
let snake_id = id.to_snake_case();
config_property_id_from_str.extend(quote! {
if s.eq_ignore_ascii_case(#id) || s.eq_ignore_ascii_case(#snake_id) {
return Ok(Self::#enum_ident);
}
});
}
let tokens = quote! {
pub trait ConfigEnum: Sized {
fn variants() -> &'static [Self];
fn variant_info() -> &'static [ConfigEnumVariantInfo];
fn as_str(&self) -> &'static str;
fn name(&self) -> &'static str;
fn description(&self) -> Option<&'static str>;
}
#[derive(Clone, Debug)]
pub struct ConfigEnumVariantInfo {
pub value: &'static str,
pub name: &'static str,
pub description: Option<&'static str>,
pub is_default: bool,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub enum ConfigPropertyId {
#property_variants
}
impl ConfigEnum for ConfigPropertyId {
#[inline]
fn variants() -> &'static [Self] {
static VARIANTS: &[ConfigPropertyId] = &[#(ConfigPropertyId::#property_idents,)*];
VARIANTS
}
#[inline]
fn variant_info() -> &'static [ConfigEnumVariantInfo] {
static VARIANT_INFO: &[ConfigEnumVariantInfo] = &[
#variant_info
];
VARIANT_INFO
}
fn as_str(&self) -> &'static str {
match self {
#config_property_id_to_str
}
}
fn name(&self) -> &'static str {
match self {
#config_property_id_to_name
}
}
fn description(&self) -> Option<&'static str> {
match self {
#config_property_id_to_description
}
}
}
impl ConfigPropertyId {
pub fn kind(&self) -> ConfigPropertyKind {
match self {
#config_property_id_to_kind
}
}
}
impl std::str::FromStr for ConfigPropertyId {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
#config_property_id_from_str
Err(())
}
}
#[derive(Clone, Debug)]
pub struct ConfigPropertyGroup {
pub id: &'static str,
pub name: &'static str,
pub description: Option<&'static str>,
pub properties: &'static [ConfigPropertyId],
}
pub static CONFIG_GROUPS: &[ConfigPropertyGroup] = &[#groups];
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub enum ConfigPropertyValue {
Boolean(bool),
Choice(&'static str),
}
impl ConfigPropertyValue {
pub fn to_json(&self) -> serde_json::Value {
match self {
ConfigPropertyValue::Boolean(value) => serde_json::Value::Bool(*value),
ConfigPropertyValue::Choice(value) => serde_json::Value::String(value.to_string()),
}
}
}
#[derive(Clone, Debug)]
pub enum ConfigPropertyKind {
Boolean,
Choice(&'static [ConfigEnumVariantInfo]),
}
#enums
#[inline(always)]
fn default_true() -> bool { true }
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify), tsify(from_wasm_abi))]
#[serde(default)]
pub struct DiffObjConfig {
#property_fields
}
impl Default for DiffObjConfig {
fn default() -> Self {
Self {
#default_fields
}
}
}
impl DiffObjConfig {
pub fn get_property_value(&self, id: ConfigPropertyId) -> ConfigPropertyValue {
match id {
#get_property_value_variants
}
}
#[allow(clippy::result_unit_err)]
pub fn set_property_value(&mut self, id: ConfigPropertyId, value: ConfigPropertyValue) -> Result<(), ()> {
match id {
#set_property_value_variants
}
}
#[allow(clippy::result_unit_err)]
pub fn set_property_value_str(&mut self, id: ConfigPropertyId, value: &str) -> Result<(), ()> {
match id {
#set_property_value_str_variants
}
}
}
};
let file = syn::parse2(tokens).unwrap();
let formatted = prettyplease::unparse(&file);
std::fs::write(
PathBuf::from(std::env::var_os("OUT_DIR").unwrap()).join("config.gen.rs"),
formatted,
)
.unwrap();
}

View File

@@ -21,9 +21,9 @@ enum SymbolFlag {
SYMBOL_NONE = 0;
SYMBOL_GLOBAL = 1;
SYMBOL_LOCAL = 2;
SYMBOL_WEAK = 4;
SYMBOL_COMMON = 8;
SYMBOL_HIDDEN = 16;
SYMBOL_WEAK = 3;
SYMBOL_COMMON = 4;
SYMBOL_HIDDEN = 5;
}
// A single parsed instruction
@@ -122,17 +122,10 @@ message InstructionBranchTo {
uint32 branch_index = 2;
}
message SymbolRef {
optional uint32 section_index = 1;
uint32 symbol_index = 2;
}
message SymbolDiff {
message FunctionDiff {
Symbol symbol = 1;
repeated InstructionDiff instructions = 2;
optional float match_percent = 3;
// The symbol ref in the _other_ object that this symbol was diffed against
optional SymbolRef target = 5;
}
message DataDiff {
@@ -147,7 +140,7 @@ message SectionDiff {
SectionKind kind = 2;
uint64 size = 3;
uint64 address = 4;
repeated SymbolDiff symbols = 5;
repeated FunctionDiff functions = 5;
repeated DataDiff data = 6;
optional float match_percent = 7;
}

View File

@@ -139,9 +139,9 @@ impl ObjArch for ObjArchArm {
let version = match config.arm_arch_version {
ArmArchVersion::Auto => self.detected_version.unwrap_or(ArmVersion::V5Te),
ArmArchVersion::V4t => ArmVersion::V4T,
ArmArchVersion::V5te => ArmVersion::V5Te,
ArmArchVersion::V6k => ArmVersion::V6K,
ArmArchVersion::V4T => ArmVersion::V4T,
ArmArchVersion::V5TE => ArmVersion::V5Te,
ArmArchVersion::V6K => ArmVersion::V6K,
};
let endian = match self.endianness {
object::Endianness::Little => unarm::Endian::Little,
@@ -276,19 +276,6 @@ impl ObjArch for ObjArchArm {
fn display_reloc(&self, flags: RelocationFlags) -> Cow<'static, str> {
Cow::Owned(format!("<{flags:?}>"))
}
fn get_reloc_byte_size(&self, flags: RelocationFlags) -> usize {
match flags {
RelocationFlags::Elf { r_type } => match r_type {
elf::R_ARM_ABS32 => 4,
elf::R_ARM_REL32 => 4,
elf::R_ARM_ABS16 => 2,
elf::R_ARM_ABS8 => 1,
_ => 1,
},
_ => 1,
}
}
}
#[derive(Clone, Copy, Debug)]

View File

@@ -173,21 +173,6 @@ impl ObjArch for ObjArchArm64 {
_ => Cow::Owned(format!("<{flags:?}>")),
}
}
fn get_reloc_byte_size(&self, flags: RelocationFlags) -> usize {
match flags {
RelocationFlags::Elf { r_type } => match r_type {
elf::R_AARCH64_ABS64 => 8,
elf::R_AARCH64_ABS32 => 4,
elf::R_AARCH64_ABS16 => 2,
elf::R_AARCH64_PREL64 => 8,
elf::R_AARCH64_PREL32 => 4,
elf::R_AARCH64_PREL16 => 2,
_ => 1,
},
_ => 1,
}
}
}
struct DisplayCtx<'a> {

View File

@@ -99,8 +99,8 @@ impl ObjArch for ObjArchMips {
MipsInstrCategory::Auto => self.instr_category,
MipsInstrCategory::Cpu => InstrCategory::CPU,
MipsInstrCategory::Rsp => InstrCategory::RSP,
MipsInstrCategory::R3000gte => InstrCategory::R3000GTE,
MipsInstrCategory::R4000allegrex => InstrCategory::R4000ALLEGREX,
MipsInstrCategory::R3000Gte => InstrCategory::R3000GTE,
MipsInstrCategory::R4000Allegrex => InstrCategory::R4000ALLEGREX,
MipsInstrCategory::R5900 => InstrCategory::R5900,
};
@@ -271,17 +271,6 @@ impl ObjArch for ObjArchMips {
_ => Cow::Owned(format!("<{flags:?}>")),
}
}
fn get_reloc_byte_size(&self, flags: RelocationFlags) -> usize {
match flags {
RelocationFlags::Elf { r_type } => match r_type {
elf::R_MIPS_16 => 2,
elf::R_MIPS_32 => 4,
_ => 1,
},
_ => 1,
}
}
}
fn push_reloc(args: &mut Vec<ObjInsArg>, reloc: &ObjReloc) -> Result<()> {

View File

@@ -36,22 +36,13 @@ pub enum DataType {
impl DataType {
pub fn display_bytes<Endian: ByteOrder>(&self, bytes: &[u8]) -> Option<String> {
if self.required_len().is_some_and(|l| bytes.len() < l) {
log::warn!("Failed to display a symbol value for a symbol whose size is too small for instruction referencing it.");
// TODO: Attempt to interpret large symbols as arrays of a smaller type,
// fallback to intrepreting it as bytes.
// https://github.com/encounter/objdiff/issues/124
if self.required_len().is_some_and(|l| bytes.len() != l) {
log::warn!("Failed to display a symbol value for a symbol whose size doesn't match the instruction referencing it.");
return None;
}
let mut bytes = bytes;
if self.required_len().is_some_and(|l| bytes.len() > l) {
// If the symbol's size is larger a single instance of this data type, we take just the
// bytes necessary for one of them in order to display the first element of the array.
bytes = &bytes[0..self.required_len().unwrap()];
// TODO: Attempt to interpret large symbols as arrays of a smaller type and show all
// elements of the array instead. https://github.com/encounter/objdiff/issues/124
// However, note that the stride of an array can not always be determined just by the
// data type guessed by the single instruction accessing it. There can also be arrays of
// structs that contain multiple elements of different types, so if other elements after
// the first one were to be displayed in this manner, they may be inaccurate.
}
match self {
DataType::Int8 => {
@@ -95,10 +86,10 @@ impl DataType {
}
}
DataType::Float => {
format!("Float: {:?}f", Endian::read_f32(bytes))
format!("Float: {}", Endian::read_f32(bytes))
}
DataType::Double => {
format!("Double: {:?}", Endian::read_f64(bytes))
format!("Double: {}", Endian::read_f64(bytes))
}
DataType::Bytes => {
format!("Bytes: {:#?}", bytes)
@@ -148,8 +139,6 @@ pub trait ObjArch: Send + Sync {
fn display_reloc(&self, flags: RelocationFlags) -> Cow<'static, str>;
fn get_reloc_byte_size(&self, flags: RelocationFlags) -> usize;
fn symbol_address(&self, symbol: &Symbol) -> u64 { symbol.address() }
fn guess_data_type(&self, _instruction: &ObjIns) -> Option<DataType> { None }
@@ -158,17 +147,6 @@ pub trait ObjArch: Send + Sync {
Some(format!("Bytes: {:#x?}", bytes))
}
fn display_ins_data(&self, ins: &ObjIns) -> Option<String> {
let reloc = ins.reloc.as_ref()?;
if reloc.addend >= 0 && reloc.target.bytes.len() > reloc.addend as usize {
self.guess_data_type(ins).and_then(|ty| {
self.display_data_type(ty, &reloc.target.bytes[reloc.addend as usize..])
})
} else {
None
}
}
// Downcast methods
#[cfg(feature = "ppc")]
fn ppc(&self) -> Option<&ppc::ObjArchPpc> { None }

View File

@@ -1,7 +1,4 @@
use std::{
borrow::Cow,
collections::{BTreeMap, HashMap, HashSet},
};
use std::{borrow::Cow, collections::BTreeMap};
use anyhow::{bail, ensure, Result};
use byteorder::BigEndian;
@@ -10,7 +7,7 @@ use object::{
elf, File, Object, ObjectSection, ObjectSymbol, Relocation, RelocationFlags, RelocationTarget,
Symbol, SymbolKind,
};
use ppc750cl::{Argument, Arguments, Ins, InsIter, Opcode, ParsedIns, GPR};
use ppc750cl::{Argument, InsIter, Opcode, GPR};
use crate::{
arch::{DataType, ObjArch, ProcessCodeResult},
@@ -52,8 +49,6 @@ impl ObjArch for ObjArchPpc {
let ins_count = code.len() / 4;
let mut ops = Vec::<u16>::with_capacity(ins_count);
let mut insts = Vec::<ObjIns>::with_capacity(ins_count);
let fake_pool_reloc_for_addr =
generate_fake_pool_reloc_for_addr_mapping(address, code, relocations);
for (cur_addr, mut ins) in InsIter::new(code, address as u32) {
let reloc = relocations.iter().find(|r| (r.address as u32 & !3) == cur_addr);
if let Some(reloc) = reloc {
@@ -143,15 +138,6 @@ impl ObjArch for ObjArchPpc {
}
}
if reloc.is_none() {
if let Some(fake_pool_reloc) = fake_pool_reloc_for_addr.get(&cur_addr) {
// If this instruction has a fake pool relocation, show it as a fake argument
// at the end of the line.
args.push(ObjInsArg::PlainText(" ".into()));
push_reloc(&mut args, fake_pool_reloc)?;
}
}
ops.push(ins.op as u16);
let line = line_info.range(..=cur_addr as u64).last().map(|(_, &b)| b);
insts.push(ObjIns {
@@ -159,7 +145,7 @@ impl ObjArch for ObjArchPpc {
size: 4,
mnemonic: Cow::Borrowed(simplified.mnemonic),
args,
reloc: reloc.or(fake_pool_reloc_for_addr.get(&cur_addr)).cloned(),
reloc: reloc.cloned(),
op: ins.op as u16,
branch_dest,
line,
@@ -187,7 +173,6 @@ impl ObjArch for ObjArchPpc {
fn display_reloc(&self, flags: RelocationFlags) -> Cow<'static, str> {
match flags {
RelocationFlags::Elf { r_type } => match r_type {
elf::R_PPC_NONE => Cow::Borrowed("R_PPC_NONE"), // We use this for fake pool relocs
elf::R_PPC_ADDR16_LO => Cow::Borrowed("R_PPC_ADDR16_LO"),
elf::R_PPC_ADDR16_HI => Cow::Borrowed("R_PPC_ADDR16_HI"),
elf::R_PPC_ADDR16_HA => Cow::Borrowed("R_PPC_ADDR16_HA"),
@@ -202,23 +187,28 @@ impl ObjArch for ObjArchPpc {
}
}
fn get_reloc_byte_size(&self, flags: RelocationFlags) -> usize {
match flags {
RelocationFlags::Elf { r_type } => match r_type {
elf::R_PPC_ADDR32 => 4,
elf::R_PPC_UADDR32 => 4,
_ => 1,
},
_ => 1,
}
}
fn guess_data_type(&self, instruction: &ObjIns) -> Option<super::DataType> {
// Always shows the first string of the table. Not ideal, but it's really hard to find
// the actual string being referenced.
if instruction.reloc.as_ref().is_some_and(|r| r.target.name.starts_with("@stringBase")) {
return Some(DataType::String);
}
guess_data_type_from_load_store_inst_op(Opcode::from(instruction.op as u8))
match Opcode::from(instruction.op as u8) {
Opcode::Lbz | Opcode::Lbzu | Opcode::Lbzux | Opcode::Lbzx => Some(DataType::Int8),
Opcode::Lhz | Opcode::Lhzu | Opcode::Lhzux | Opcode::Lhzx => Some(DataType::Int16),
Opcode::Lha | Opcode::Lhau | Opcode::Lhaux | Opcode::Lhax => Some(DataType::Int16),
Opcode::Lwz | Opcode::Lwzu | Opcode::Lwzux | Opcode::Lwzx => Some(DataType::Int32),
Opcode::Lfs | Opcode::Lfsu | Opcode::Lfsux | Opcode::Lfsx => Some(DataType::Float),
Opcode::Lfd | Opcode::Lfdu | Opcode::Lfdux | Opcode::Lfdx => Some(DataType::Double),
Opcode::Stb | Opcode::Stbu | Opcode::Stbux | Opcode::Stbx => Some(DataType::Int8),
Opcode::Sth | Opcode::Sthu | Opcode::Sthux | Opcode::Sthx => Some(DataType::Int16),
Opcode::Stw | Opcode::Stwu | Opcode::Stwux | Opcode::Stwx => Some(DataType::Int32),
Opcode::Stfs | Opcode::Stfsu | Opcode::Stfsux | Opcode::Stfsx => Some(DataType::Float),
Opcode::Stfd | Opcode::Stfdu | Opcode::Stfdux | Opcode::Stfdx => Some(DataType::Double),
_ => None,
}
}
fn display_data_type(&self, ty: DataType, bytes: &[u8]) -> Option<String> {
@@ -256,12 +246,6 @@ fn push_reloc(args: &mut Vec<ObjInsArg>, reloc: &ObjReloc) -> Result<()> {
elf::R_PPC_ADDR32 | elf::R_PPC_UADDR32 | elf::R_PPC_REL24 | elf::R_PPC_REL14 => {
args.push(ObjInsArg::Reloc);
}
elf::R_PPC_NONE => {
// Fake pool relocation.
args.push(ObjInsArg::PlainText("<".into()));
args.push(ObjInsArg::Reloc);
args.push(ObjInsArg::PlainText(">".into()));
}
_ => bail!("Unsupported ELF PPC relocation type {r_type}"),
},
flags => bail!("Unsupported PPC relocation kind: {flags:?}"),
@@ -397,345 +381,3 @@ fn make_symbol_ref(symbol: &Symbol) -> Result<ExtabSymbolRef> {
let demangled_name = cwdemangle::demangle(&name, &cwdemangle::DemangleOptions::default());
Ok(ExtabSymbolRef { original_index: symbol.index().0, name, demangled_name })
}
fn guess_data_type_from_load_store_inst_op(inst_op: Opcode) -> Option<DataType> {
match inst_op {
Opcode::Lbz | Opcode::Lbzu | Opcode::Lbzux | Opcode::Lbzx => Some(DataType::Int8),
Opcode::Lhz | Opcode::Lhzu | Opcode::Lhzux | Opcode::Lhzx => Some(DataType::Int16),
Opcode::Lha | Opcode::Lhau | Opcode::Lhaux | Opcode::Lhax => Some(DataType::Int16),
Opcode::Lwz | Opcode::Lwzu | Opcode::Lwzux | Opcode::Lwzx => Some(DataType::Int32),
Opcode::Lfs | Opcode::Lfsu | Opcode::Lfsux | Opcode::Lfsx => Some(DataType::Float),
Opcode::Lfd | Opcode::Lfdu | Opcode::Lfdux | Opcode::Lfdx => Some(DataType::Double),
Opcode::Stb | Opcode::Stbu | Opcode::Stbux | Opcode::Stbx => Some(DataType::Int8),
Opcode::Sth | Opcode::Sthu | Opcode::Sthux | Opcode::Sthx => Some(DataType::Int16),
Opcode::Stw | Opcode::Stwu | Opcode::Stwux | Opcode::Stwx => Some(DataType::Int32),
Opcode::Stfs | Opcode::Stfsu | Opcode::Stfsux | Opcode::Stfsx => Some(DataType::Float),
Opcode::Stfd | Opcode::Stfdu | Opcode::Stfdux | Opcode::Stfdx => Some(DataType::Double),
_ => None,
}
}
// Given an instruction, determine if it could accessing data at the address in a register.
// If so, return the offset added to the register's address, the register containing that address,
// and (optionally) which destination register the address is being copied into.
fn get_offset_and_addr_gpr_for_possible_pool_reference(
opcode: Opcode,
simplified: &ParsedIns,
) -> Option<(i16, GPR, Option<GPR>)> {
let args = &simplified.args;
if guess_data_type_from_load_store_inst_op(opcode).is_some() {
match (args[1], args[2]) {
(Argument::Offset(offset), Argument::GPR(addr_src_gpr)) => {
// e.g. lwz. Immediate offset.
Some((offset.0, addr_src_gpr, None))
}
(Argument::GPR(addr_src_gpr), Argument::GPR(_offset_gpr)) => {
// e.g. lwzx. The offset is in a register and was likely calculated from an index.
// Treat the offset as being 0 in this case to show the first element of the array.
// It may be possible to show all elements by figuring out the stride of the array
// from the calculations performed on the index before it's put into offset_gpr, but
// this would be much more complicated, so it's not currently done.
Some((0, addr_src_gpr, None))
}
_ => None,
}
} else {
// If it's not a load/store instruction, there's two more possibilities we need to handle.
// 1. It could be loading a pointer to a string.
// 2. It could be moving the relocation address plus an offset into a different register to
// load from later.
// If either of these match, we also want to return the destination register that the
// address is being copied into so that we can detect any future references to that new
// register as well.
match (opcode, args[0], args[1], args[2]) {
(
Opcode::Addi,
Argument::GPR(addr_dst_gpr),
Argument::GPR(addr_src_gpr),
Argument::Simm(simm),
) => Some((simm.0, addr_src_gpr, Some(addr_dst_gpr))),
(
// `mr` or `mr.`
Opcode::Or,
Argument::GPR(addr_dst_gpr),
Argument::GPR(addr_src_gpr),
Argument::None,
) => Some((0, addr_src_gpr, Some(addr_dst_gpr))),
(
Opcode::Add,
Argument::GPR(addr_dst_gpr),
Argument::GPR(addr_src_gpr),
Argument::GPR(_offset_gpr),
) => Some((0, addr_src_gpr, Some(addr_dst_gpr))),
_ => None,
}
}
}
// Remove the relocation we're keeping track of in a particular register when an instruction reuses
// that register to hold some other value, unrelated to pool relocation addresses.
fn clear_overwritten_gprs(ins: Ins, gpr_pool_relocs: &mut HashMap<u8, ObjReloc>) {
let mut def_args = Arguments::default();
ins.parse_defs(&mut def_args);
for arg in def_args {
if let Argument::GPR(gpr) = arg {
if ins.op == Opcode::Lmw {
// `lmw` overwrites all registers from rd to r31.
// ppc750cl only returns rd itself, so we manually clear the rest of them.
for reg in gpr.0..31 {
gpr_pool_relocs.remove(&reg);
}
break;
}
gpr_pool_relocs.remove(&gpr.0);
}
}
}
// We create a fake relocation for an instruction, vaguely simulating what the actual relocation
// might have looked like if it wasn't pooled. This is so minimal changes are needed to display
// pooled accesses vs non-pooled accesses. We set the relocation type to R_PPC_NONE to indicate that
// there isn't really a relocation here, as copying the pool relocation's type wouldn't make sense.
// Also, if this instruction is accessing the middle of a symbol instead of the start, we add an
// addend to indicate that.
fn make_fake_pool_reloc(offset: i16, cur_addr: u32, pool_reloc: &ObjReloc) -> Option<ObjReloc> {
let offset_from_pool = pool_reloc.addend + offset as i64;
let target_address = pool_reloc.target.address.checked_add_signed(offset_from_pool)?;
let target;
let addend;
if pool_reloc.target.orig_section_index.is_some() {
// If the target symbol is within this current object, then we also need to create a fake
// target symbol to go inside our fake relocation. This is because we don't have access to
// list of all symbols in this section, so we can't find the real symbol within the pool
// based on its address yet. Instead we make a placeholder that has the correct
// `orig_section_index` and `address` fields, and then later on when this information is
// displayed to the user, we can find the real symbol by searching through the object's
// section's symbols for one that contains this address.
target = ObjSymbol {
name: "".to_string(),
demangled_name: None,
address: target_address,
section_address: 0,
size: 0,
size_known: false,
kind: Default::default(),
flags: Default::default(),
orig_section_index: pool_reloc.target.orig_section_index,
virtual_address: None,
original_index: None,
bytes: vec![],
};
// The addend is also fake because we don't know yet if the `target_address` here is the exact
// start of the symbol or if it's in the middle of it.
addend = 0;
} else {
// But if the target symbol is in a different object (extern), then we simply copy the pool
// relocation's target. This is because it won't be possible to locate the actual symbol
// later on based only off of an offset without knowing the object or section it's in. And
// doing that for external symbols would also be unnecessary, because when the compiler
// generates an instruction that accesses an external "pool" plus some offset, that won't be
// a normal pool that contains other symbols within it that we want to display. It will be
// something like a vtable for a class with multiple inheritance (for example, dCcD_Cyl in
// The Wind Waker). So just showing that vtable symbol plus an addend to represent the
// offset into it works fine in this case, no fake symbol to hold an address is necessary.
target = pool_reloc.target.clone();
addend = pool_reloc.addend;
};
Some(ObjReloc {
flags: RelocationFlags::Elf { r_type: elf::R_PPC_NONE },
address: cur_addr as u64,
target,
addend,
})
}
// Searches through all instructions in a function, determining which registers have the addresses
// of pooled data relocations in them, finding which instructions load data from those addresses,
// and constructing a mapping of the address of that instruction to a "fake pool relocation" that
// simulates what that instruction's relocation would look like if data hadn't been pooled.
// This method tries to follow the function's proper control flow. It keeps track of a queue of
// states it hasn't traversed yet, where each state holds an instruction address and a HashMap of
// which registers hold which pool relocations at that point.
// When a conditional or unconditional branch is encountered, the destination of the branch is added
// to the queue. Conditional branches will traverse both the path where the branch is taken and the
// one where it's not. Unconditional branches only follow the branch, ignoring any code immediately
// after the branch instruction.
// Limitations: This method cannot read jump tables. This is because the jump tables are located in
// the .data section, but ObjArch.process_code only has access to the .text section. In order to
// work around this limitation and avoid completely missing most code inside switch statements that
// use jump tables, we instead guess that any parts of a function we missed were switch cases, and
// traverse them as if the last `bctr` before that address had branched there. This should be fairly
// accurate in practice - in testing the only instructions it seems to miss are double branches that
// the compiler generates in error which can never be reached during normal execution anyway.
fn generate_fake_pool_reloc_for_addr_mapping(
func_address: u64,
code: &[u8],
relocations: &[ObjReloc],
) -> HashMap<u32, ObjReloc> {
let mut visited_ins_addrs = HashSet::new();
let mut pool_reloc_for_addr = HashMap::new();
let mut ins_iters_with_gpr_state =
vec![(InsIter::new(code, func_address as u32), HashMap::new())];
let mut gpr_state_at_bctr = BTreeMap::new();
while let Some((ins_iter, mut gpr_pool_relocs)) = ins_iters_with_gpr_state.pop() {
for (cur_addr, ins) in ins_iter {
if visited_ins_addrs.contains(&cur_addr) {
// Avoid getting stuck in an infinite loop when following looping branches.
break;
}
visited_ins_addrs.insert(cur_addr);
let simplified = ins.simplified();
// First handle traversing the function's control flow.
let mut branch_dest = None;
for arg in simplified.args_iter() {
if let Argument::BranchDest(dest) = arg {
let dest = cur_addr.wrapping_add_signed(dest.0);
branch_dest = Some(dest);
break;
}
}
if let Some(branch_dest) = branch_dest {
if branch_dest >= func_address as u32
&& (branch_dest - func_address as u32) < code.len() as u32
{
let dest_offset_into_func = branch_dest - func_address as u32;
let dest_code_slice = &code[dest_offset_into_func as usize..];
match ins.op {
Opcode::Bc => {
// Conditional branch.
// Add the branch destination to the queue to do later.
ins_iters_with_gpr_state.push((
InsIter::new(dest_code_slice, branch_dest),
gpr_pool_relocs.clone(),
));
// Then continue on with the current iterator.
}
Opcode::B => {
if simplified.mnemonic != "bl" {
// Unconditional branch.
// Add the branch destination to the queue.
ins_iters_with_gpr_state.push((
InsIter::new(dest_code_slice, branch_dest),
gpr_pool_relocs.clone(),
));
// Break out of the current iterator so we can do the newly added one.
break;
}
}
_ => unreachable!(),
}
}
}
if let Opcode::Bcctr = ins.op {
if simplified.mnemonic == "bctr" {
// Unconditional branch to count register.
// Likely a jump table.
gpr_state_at_bctr.insert(cur_addr, gpr_pool_relocs.clone());
}
}
// Then handle keeping track of which GPR contains which pool relocation.
let reloc = relocations.iter().find(|r| (r.address as u32 & !3) == cur_addr);
if let Some(reloc) = reloc {
// This instruction has a real relocation, so it may be a pool load we want to keep
// track of.
let args = &simplified.args;
match (ins.op, args[0], args[1], args[2]) {
(
// `lis` + `addi`
Opcode::Addi,
Argument::GPR(addr_dst_gpr),
Argument::GPR(_addr_src_gpr),
Argument::Simm(_simm),
) => {
gpr_pool_relocs.insert(addr_dst_gpr.0, reloc.clone());
}
(
// `lis` + `ori`
Opcode::Ori,
Argument::GPR(addr_dst_gpr),
Argument::GPR(_addr_src_gpr),
Argument::Uimm(_uimm),
) => {
gpr_pool_relocs.insert(addr_dst_gpr.0, reloc.clone());
}
(Opcode::B, _, _, _) => {
if simplified.mnemonic == "bl" {
// When encountering a function call, clear any active pool relocations from
// the volatile registers (r0, r3-r12), but not the nonvolatile registers.
gpr_pool_relocs.remove(&0);
for gpr in 3..12 {
gpr_pool_relocs.remove(&gpr);
}
}
}
_ => {
clear_overwritten_gprs(ins, &mut gpr_pool_relocs);
}
}
} else if let Some((offset, addr_src_gpr, addr_dst_gpr)) =
get_offset_and_addr_gpr_for_possible_pool_reference(ins.op, &simplified)
{
// This instruction doesn't have a real relocation, so it may be a reference to one of
// the already-loaded pools.
if let Some(pool_reloc) = gpr_pool_relocs.get(&addr_src_gpr.0) {
if let Some(fake_pool_reloc) =
make_fake_pool_reloc(offset, cur_addr, pool_reloc)
{
pool_reloc_for_addr.insert(cur_addr, fake_pool_reloc);
}
if let Some(addr_dst_gpr) = addr_dst_gpr {
// If the address of the pool relocation got copied into another register, we
// need to keep track of it in that register too as future instructions may
// reference the symbol indirectly via this new register, instead of the
// register the symbol's address was originally loaded into.
// For example, the start of the function might `lis` + `addi` the start of the
// ...data pool into r25, and then later the start of a loop will `addi` r25
// with the offset within the .data section of an array variable into r21.
// Then the body of the loop will `lwzx` one of the array elements from r21.
let mut new_reloc = pool_reloc.clone();
new_reloc.addend += offset as i64;
gpr_pool_relocs.insert(addr_dst_gpr.0, new_reloc);
} else {
clear_overwritten_gprs(ins, &mut gpr_pool_relocs);
}
} else {
clear_overwritten_gprs(ins, &mut gpr_pool_relocs);
}
} else {
clear_overwritten_gprs(ins, &mut gpr_pool_relocs);
}
}
// Finally, if we're about to finish the outer loop and don't have any more control flow to
// follow, we check if there are any instruction addresses in this function that we missed.
// If so, and if there were any `bctr` instructions before those points in this function,
// then we try to traverse those missing spots as switch cases.
if ins_iters_with_gpr_state.is_empty() {
let unseen_addrs = (func_address as u32..func_address as u32 + code.len() as u32)
.step_by(4)
.filter(|addr| !visited_ins_addrs.contains(addr));
for unseen_addr in unseen_addrs {
let prev_bctr_gpr_state = gpr_state_at_bctr
.iter()
.filter(|(&addr, _)| addr < unseen_addr)
.min_by_key(|(&addr, _)| addr)
.map(|(_, gpr_state)| gpr_state);
if let Some(gpr_pool_relocs) = prev_bctr_gpr_state {
let dest_offset_into_func = unseen_addr - func_address as u32;
let dest_code_slice = &code[dest_offset_into_func as usize..];
ins_iters_with_gpr_state.push((
InsIter::new(dest_code_slice, unseen_addr),
gpr_pool_relocs.clone(),
));
break;
}
}
}
}
pool_reloc_for_addr
}

View File

@@ -162,19 +162,6 @@ impl ObjArch for ObjArchX86 {
_ => Cow::Owned(format!("<{flags:?}>")),
}
}
fn get_reloc_byte_size(&self, flags: RelocationFlags) -> usize {
match flags {
RelocationFlags::Coff { typ } => match typ {
pe::IMAGE_REL_I386_DIR16 => 2,
pe::IMAGE_REL_I386_REL16 => 2,
pe::IMAGE_REL_I386_DIR32 => 4,
pe::IMAGE_REL_I386_REL32 => 4,
_ => 1,
},
_ => 1,
}
}
}
fn replace_arg(

View File

@@ -4,7 +4,6 @@ use crate::{
ObjDataDiff, ObjDataDiffKind, ObjDiff, ObjInsArgDiff, ObjInsBranchFrom, ObjInsBranchTo,
ObjInsDiff, ObjInsDiffKind, ObjSectionDiff, ObjSymbolDiff,
},
obj,
obj::{
ObjInfo, ObjIns, ObjInsArg, ObjInsArgValue, ObjReloc, ObjSectionKind, ObjSymbol,
ObjSymbolFlagSet, ObjSymbolFlags,
@@ -40,15 +39,14 @@ impl ObjectDiff {
impl SectionDiff {
pub fn new(obj: &ObjInfo, section_index: usize, section_diff: &ObjSectionDiff) -> Self {
let section = &obj.sections[section_index];
let symbols = section_diff.symbols.iter().map(|d| SymbolDiff::new(obj, d)).collect();
let functions = section_diff.symbols.iter().map(|d| FunctionDiff::new(obj, d)).collect();
let data = section_diff.data_diff.iter().map(|d| DataDiff::new(obj, d)).collect();
// TODO: section_diff.reloc_diff
Self {
name: section.name.to_string(),
kind: SectionKind::from(section.kind) as i32,
size: section.size,
address: section.address,
symbols,
functions,
data,
match_percent: section_diff.match_percent,
}
@@ -66,22 +64,13 @@ impl From<ObjSectionKind> for SectionKind {
}
}
impl From<obj::SymbolRef> for SymbolRef {
fn from(value: obj::SymbolRef) -> Self {
Self {
section_index: if value.section_idx == obj::SECTION_COMMON {
None
} else {
Some(value.section_idx as u32)
},
symbol_index: value.symbol_idx as u32,
}
}
}
impl SymbolDiff {
impl FunctionDiff {
pub fn new(object: &ObjInfo, symbol_diff: &ObjSymbolDiff) -> Self {
let (_section, symbol) = object.section_symbol(symbol_diff.symbol_ref);
// let diff_symbol = symbol_diff.diff_symbol.map(|symbol_ref| {
// let (_section, symbol) = object.section_symbol(symbol_ref);
// Symbol::from(symbol)
// });
let instructions = symbol_diff
.instructions
.iter()
@@ -89,9 +78,9 @@ impl SymbolDiff {
.collect();
Self {
symbol: Some(Symbol::new(symbol)),
// diff_symbol,
instructions,
match_percent: symbol_diff.match_percent,
target: symbol_diff.target_symbol.map(SymbolRef::from),
}
}
}
@@ -121,7 +110,7 @@ impl Symbol {
fn symbol_flags(value: ObjSymbolFlagSet) -> u32 {
let mut flags = 0u32;
if value.0.contains(ObjSymbolFlags::Global) {
flags |= SymbolFlag::SymbolGlobal as u32;
flags |= SymbolFlag::SymbolNone as u32;
}
if value.0.contains(ObjSymbolFlags::Local) {
flags |= SymbolFlag::SymbolLocal as u32;

View File

@@ -13,22 +13,20 @@ fn parse_object(
fn parse_and_run_diff(
left: Option<Box<[u8]>>,
right: Option<Box<[u8]>>,
diff_config: diff::DiffObjConfig,
mapping_config: diff::MappingConfig,
config: diff::DiffObjConfig,
) -> Result<DiffResult, JsError> {
let target = parse_object(left, &diff_config)?;
let base = parse_object(right, &diff_config)?;
run_diff(target.as_ref(), base.as_ref(), diff_config, mapping_config)
let target = parse_object(left, &config)?;
let base = parse_object(right, &config)?;
run_diff(target.as_ref(), base.as_ref(), config)
}
fn run_diff(
left: Option<&obj::ObjInfo>,
right: Option<&obj::ObjInfo>,
diff_config: diff::DiffObjConfig,
mapping_config: diff::MappingConfig,
config: diff::DiffObjConfig,
) -> Result<DiffResult, JsError> {
log::debug!("Running diff with config: {:?}", diff_config);
let result = diff::diff_objs(&diff_config, &mapping_config, left, right, None).to_js()?;
log::debug!("Running diff with config: {:?}", config);
let result = diff::diff_objs(&config, left, right, None).to_js()?;
let left = left.and_then(|o| result.left.as_ref().map(|d| (o, d)));
let right = right.and_then(|o| result.right.as_ref().map(|d| (o, d)));
Ok(DiffResult::new(left, right))
@@ -48,10 +46,9 @@ fn run_diff(
pub fn run_diff_proto(
left: Option<Box<[u8]>>,
right: Option<Box<[u8]>>,
diff_config: diff::DiffObjConfig,
mapping_config: diff::MappingConfig,
config: diff::DiffObjConfig,
) -> Result<Box<[u8]>, JsError> {
let out = parse_and_run_diff(left, right, diff_config, mapping_config)?;
let out = parse_and_run_diff(left, right, config)?;
Ok(out.encode_to_vec().into_boxed_slice())
}

View File

@@ -1,106 +0,0 @@
pub mod watcher;
use std::{
path::{Path, PathBuf},
process::Command,
};
pub struct BuildStatus {
pub success: bool,
pub cmdline: String,
pub stdout: String,
pub stderr: String,
}
impl Default for BuildStatus {
fn default() -> Self {
BuildStatus {
success: true,
cmdline: String::new(),
stdout: String::new(),
stderr: String::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct BuildConfig {
pub project_dir: Option<PathBuf>,
pub custom_make: Option<String>,
pub custom_args: Option<Vec<String>>,
#[allow(unused)]
pub selected_wsl_distro: Option<String>,
}
pub fn run_make(config: &BuildConfig, arg: &Path) -> BuildStatus {
let Some(cwd) = &config.project_dir else {
return BuildStatus {
success: false,
stderr: "Missing project dir".to_string(),
..Default::default()
};
};
let make = config.custom_make.as_deref().unwrap_or("make");
let make_args = config.custom_args.as_deref().unwrap_or(&[]);
#[cfg(not(windows))]
let mut command = {
let mut command = Command::new(make);
command.current_dir(cwd).args(make_args).arg(arg);
command
};
#[cfg(windows)]
let mut command = {
use std::os::windows::process::CommandExt;
use path_slash::PathExt;
let mut command = if config.selected_wsl_distro.is_some() {
Command::new("wsl")
} else {
Command::new(make)
};
if let Some(distro) = &config.selected_wsl_distro {
// Strip distro root prefix \\wsl.localhost\{distro}
let wsl_path_prefix = format!("\\\\wsl.localhost\\{}", distro);
let cwd = match cwd.strip_prefix(wsl_path_prefix) {
Ok(new_cwd) => format!("/{}", new_cwd.to_slash_lossy().as_ref()),
Err(_) => cwd.to_string_lossy().to_string(),
};
command
.arg("--cd")
.arg(cwd)
.arg("-d")
.arg(distro)
.arg("--")
.arg(make)
.args(make_args)
.arg(arg.to_slash_lossy().as_ref());
} else {
command.current_dir(cwd).args(make_args).arg(arg.to_slash_lossy().as_ref());
}
command.creation_flags(winapi::um::winbase::CREATE_NO_WINDOW);
command
};
let mut cmdline = shell_escape::escape(command.get_program().to_string_lossy()).into_owned();
for arg in command.get_args() {
cmdline.push(' ');
cmdline.push_str(shell_escape::escape(arg.to_string_lossy()).as_ref());
}
let output = match command.output() {
Ok(output) => output,
Err(e) => {
return BuildStatus {
success: false,
cmdline,
stdout: Default::default(),
stderr: e.to_string(),
};
}
};
// Try from_utf8 first to avoid copying the buffer if it's valid, then fall back to from_utf8_lossy
let stdout = String::from_utf8(output.stdout)
.unwrap_or_else(|e| String::from_utf8_lossy(e.as_bytes()).into_owned());
let stderr = String::from_utf8(output.stderr)
.unwrap_or_else(|e| String::from_utf8_lossy(e.as_bytes()).into_owned());
BuildStatus { success: output.status.success(), cmdline, stdout, stderr }
}

View File

@@ -1,75 +0,0 @@
use std::{
fs,
path::{Path, PathBuf},
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
task::Waker,
time::Duration,
};
use globset::GlobSet;
use notify::RecursiveMode;
use notify_debouncer_full::{new_debouncer_opt, DebounceEventResult};
pub type Watcher = notify_debouncer_full::Debouncer<
notify::RecommendedWatcher,
notify_debouncer_full::RecommendedCache,
>;
pub struct WatcherState {
pub config_path: Option<PathBuf>,
pub left_obj_path: Option<PathBuf>,
pub right_obj_path: Option<PathBuf>,
pub patterns: GlobSet,
}
pub fn create_watcher(
modified: Arc<AtomicBool>,
project_dir: &Path,
patterns: GlobSet,
waker: Waker,
) -> notify::Result<Watcher> {
let base_dir = fs::canonicalize(project_dir)?;
let base_dir_clone = base_dir.clone();
let timeout = Duration::from_millis(200);
let config = notify::Config::default().with_poll_interval(Duration::from_secs(2));
let mut debouncer = new_debouncer_opt(
timeout,
None,
move |result: DebounceEventResult| match result {
Ok(events) => {
let mut any_match = false;
for event in events.iter() {
if !matches!(
event.kind,
notify::EventKind::Modify(..)
| notify::EventKind::Create(..)
| notify::EventKind::Remove(..)
) {
continue;
}
for path in &event.paths {
let Ok(path) = path.strip_prefix(&base_dir_clone) else {
continue;
};
if patterns.is_match(path) {
// log::info!("File modified: {}", path.display());
any_match = true;
}
}
}
if any_match {
modified.store(true, Ordering::Relaxed);
waker.wake_by_ref();
}
}
Err(errors) => errors.iter().for_each(|e| log::error!("Watch error: {e:?}")),
},
notify_debouncer_full::RecommendedCache::new(),
config,
)?;
debouncer.watch(base_dir, RecursiveMode::Recursive)?;
Ok(debouncer)
}

View File

@@ -1,5 +1,4 @@
use std::{
collections::BTreeMap,
fs,
fs::File,
io::{BufReader, BufWriter, Read},
@@ -7,11 +6,11 @@ use std::{
};
use anyhow::{anyhow, Context, Result};
use bimap::BiBTreeMap;
use filetime::FileTime;
use globset::{Glob, GlobSet, GlobSetBuilder};
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify), tsify(from_wasm_abi))]
pub struct ProjectConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub min_version: Option<String>,
@@ -28,7 +27,7 @@ pub struct ProjectConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub build_target: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub watch_patterns: Option<Vec<String>>,
pub watch_patterns: Option<Vec<Glob>>,
#[serde(default, alias = "objects", skip_serializing_if = "Option::is_none")]
pub units: Option<Vec<ProjectObject>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -53,21 +52,9 @@ impl ProjectConfig {
pub fn progress_categories_mut(&mut self) -> &mut Vec<ProjectProgressCategory> {
self.progress_categories.get_or_insert_with(Vec::new)
}
pub fn build_watch_patterns(&self) -> Result<Vec<Glob>, globset::Error> {
Ok(if let Some(watch_patterns) = &self.watch_patterns {
watch_patterns
.iter()
.map(|s| Glob::new(s))
.collect::<Result<Vec<Glob>, globset::Error>>()?
} else {
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect()
})
}
}
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify), tsify(from_wasm_abi))]
pub struct ProjectObject {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
@@ -91,11 +78,9 @@ pub struct ProjectObject {
pub symbol_mappings: Option<SymbolMappings>,
}
#[cfg_attr(feature = "wasm", tsify_next::declare)]
pub type SymbolMappings = BTreeMap<String, String>;
pub type SymbolMappings = BiBTreeMap<String, String>;
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify), tsify(from_wasm_abi))]
pub struct ProjectObjectMetadata {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub complete: Option<bool>,
@@ -110,7 +95,6 @@ pub struct ProjectObjectMetadata {
}
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify), tsify(from_wasm_abi))]
pub struct ProjectProgressCategory {
#[serde(default)]
pub id: String,
@@ -170,7 +154,6 @@ impl ProjectObject {
}
#[derive(Default, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify), tsify(from_wasm_abi))]
pub struct ScratchConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub platform: Option<String>,
@@ -193,10 +176,6 @@ pub const DEFAULT_WATCH_PATTERNS: &[&str] = &[
"*.inc", "*.py", "*.yml", "*.txt", "*.json",
];
pub fn default_watch_patterns() -> Vec<Glob> {
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect()
}
#[derive(Clone, Eq, PartialEq)]
pub struct ProjectConfigInfo {
pub path: PathBuf,

View File

@@ -3,17 +3,13 @@ use std::{cmp::max, collections::BTreeMap};
use anyhow::{anyhow, Result};
use similar::{capture_diff_slices_deadline, Algorithm};
use super::FunctionRelocDiffs;
use crate::{
arch::ProcessCodeResult,
diff::{
DiffObjConfig, ObjInsArgDiff, ObjInsBranchFrom, ObjInsBranchTo, ObjInsDiff, ObjInsDiffKind,
ObjSymbolDiff,
},
obj::{
ObjInfo, ObjIns, ObjInsArg, ObjReloc, ObjSection, ObjSymbol, ObjSymbolFlags, ObjSymbolKind,
SymbolRef,
},
obj::{ObjInfo, ObjInsArg, ObjReloc, ObjSymbolFlags, SymbolRef},
};
pub fn process_code_symbol(
@@ -25,30 +21,14 @@ pub fn process_code_symbol(
let section = section.ok_or_else(|| anyhow!("Code symbol section not found"))?;
let code = &section.data
[symbol.section_address as usize..(symbol.section_address + symbol.size) as usize];
let mut res = obj.arch.process_code(
obj.arch.process_code(
symbol.address,
code,
section.orig_index,
&section.relocations,
&section.line_info,
config,
)?;
for inst in res.insts.iter_mut() {
if let Some(reloc) = &mut inst.reloc {
if reloc.target.size == 0 && reloc.target.name.is_empty() {
// Fake target symbol we added as a placeholder. We need to find the real one.
if let Some(real_target) =
find_symbol_matching_fake_symbol_in_sections(&reloc.target, &obj.sections)
{
reloc.addend = (reloc.target.address - real_target.address) as i64;
reloc.target = real_target;
}
}
}
}
Ok(res)
)
}
pub fn no_diff_code(out: &ProcessCodeResult, symbol_ref: SymbolRef) -> Result<ObjSymbolDiff> {
@@ -196,7 +176,7 @@ fn address_eq(left: &ObjReloc, right: &ObjReloc) -> bool {
left.target.address as i64 + left.addend == right.target.address as i64 + right.addend
}
pub fn section_name_eq(
fn section_name_eq(
left_obj: &ObjInfo,
right_obj: &ObjInfo,
left_orig_section_index: usize,
@@ -219,19 +199,16 @@ fn reloc_eq(
config: &DiffObjConfig,
left_obj: &ObjInfo,
right_obj: &ObjInfo,
left_ins: Option<&ObjIns>,
right_ins: Option<&ObjIns>,
left_reloc: Option<&ObjReloc>,
right_reloc: Option<&ObjReloc>,
) -> bool {
let (Some(left_ins), Some(right_ins)) = (left_ins, right_ins) else {
return false;
};
let (Some(left), Some(right)) = (&left_ins.reloc, &right_ins.reloc) else {
let (Some(left), Some(right)) = (left_reloc, right_reloc) else {
return false;
};
if left.flags != right.flags {
return false;
}
if config.function_reloc_diffs == FunctionRelocDiffs::None {
if config.relax_reloc_diffs {
return true;
}
@@ -240,13 +217,7 @@ fn reloc_eq(
(Some(sl), Some(sr)) => {
// Match if section and name or address match
section_name_eq(left_obj, right_obj, *sl, *sr)
&& (config.function_reloc_diffs == FunctionRelocDiffs::DataValue
|| symbol_name_matches
|| address_eq(left, right))
&& (config.function_reloc_diffs == FunctionRelocDiffs::NameAddress
|| left.target.kind != ObjSymbolKind::Object
|| left_obj.arch.display_ins_data(left_ins)
== left_obj.arch.display_ins_data(right_ins))
&& (symbol_name_matches || address_eq(left, right))
}
(Some(_), None) => false,
(None, Some(_)) => {
@@ -272,10 +243,10 @@ fn arg_eq(
_ => false,
},
ObjInsArg::Arg(l) => match right {
ObjInsArg::Arg(r) => l.loose_eq(r),
ObjInsArg::Arg(r) => l == r,
// If relocations are relaxed, match if left is a constant and right is a reloc
// Useful for instances where the target object is created without relocations
ObjInsArg::Reloc => config.function_reloc_diffs == FunctionRelocDiffs::None,
ObjInsArg::Reloc => config.relax_reloc_diffs,
_ => false,
},
ObjInsArg::Reloc => {
@@ -284,8 +255,8 @@ fn arg_eq(
config,
left_obj,
right_obj,
left_diff.ins.as_ref(),
right_diff.ins.as_ref(),
left_diff.ins.as_ref().and_then(|i| i.reloc.as_ref()),
right_diff.ins.as_ref().and_then(|i| i.reloc.as_ref()),
)
}
ObjInsArg::BranchDest(_) => match right {
@@ -296,7 +267,7 @@ fn arg_eq(
}
// If relocations are relaxed, match if left is a constant and right is a reloc
// Useful for instances where the target object is created without relocations
ObjInsArg::Reloc => config.function_reloc_diffs == FunctionRelocDiffs::None,
ObjInsArg::Reloc => config.relax_reloc_diffs,
_ => false,
},
}
@@ -398,16 +369,3 @@ fn compare_ins(
}
Ok(result)
}
fn find_symbol_matching_fake_symbol_in_sections(
fake_symbol: &ObjSymbol,
sections: &[ObjSection],
) -> Option<ObjSymbol> {
let orig_section_index = fake_symbol.orig_section_index?;
let section = sections.iter().find(|s| s.orig_index == orig_section_index)?;
let real_symbol = section
.symbols
.iter()
.find(|s| s.size > 0 && (s.address..s.address + s.size).contains(&fake_symbol.address))?;
Some(real_symbol.clone())
}

View File

@@ -1,15 +1,11 @@
use std::{
cmp::{max, min, Ordering},
ops::Range,
};
use std::cmp::{max, min, Ordering};
use anyhow::{anyhow, Result};
use similar::{capture_diff_slices_deadline, get_diff_ratio, Algorithm};
use super::code::section_name_eq;
use crate::{
diff::{ObjDataDiff, ObjDataDiffKind, ObjDataRelocDiff, ObjSectionDiff, ObjSymbolDiff},
obj::{ObjInfo, ObjReloc, ObjSection, ObjSymbolFlags, SymbolRef},
diff::{ObjDataDiff, ObjDataDiffKind, ObjSectionDiff, ObjSymbolDiff},
obj::{ObjInfo, ObjSection, SymbolRef},
};
pub fn diff_bss_symbol(
@@ -41,110 +37,8 @@ pub fn no_diff_symbol(_obj: &ObjInfo, symbol_ref: SymbolRef) -> ObjSymbolDiff {
ObjSymbolDiff { symbol_ref, target_symbol: None, instructions: vec![], match_percent: None }
}
fn address_eq(left: &ObjReloc, right: &ObjReloc) -> bool {
if right.target.size == 0 && left.target.size != 0 {
// The base relocation is against a pool but the target relocation isn't.
// This can happen in rare cases where the compiler will generate a pool+addend relocation
// in the base, but the one detected in the target is direct with no addend.
// Just check that the final address is the same so these count as a match.
left.target.address as i64 + left.addend == right.target.address as i64 + right.addend
} else {
// But otherwise, if the compiler isn't using a pool, we're more strict and check that the
// target symbol address and relocation addend both match exactly.
left.target.address == right.target.address && left.addend == right.addend
}
}
fn reloc_eq(left_obj: &ObjInfo, right_obj: &ObjInfo, left: &ObjReloc, right: &ObjReloc) -> bool {
if left.flags != right.flags {
return false;
}
let symbol_name_matches = left.target.name == right.target.name;
match (&left.target.orig_section_index, &right.target.orig_section_index) {
(Some(sl), Some(sr)) => {
// Match if section and name+addend or address match
section_name_eq(left_obj, right_obj, *sl, *sr)
&& ((symbol_name_matches && left.addend == right.addend) || address_eq(left, right))
}
(Some(_), None) => false,
(None, Some(_)) => {
// Match if possibly stripped weak symbol
(symbol_name_matches && left.addend == right.addend)
&& right.target.flags.0.contains(ObjSymbolFlags::Weak)
}
(None, None) => symbol_name_matches,
}
}
/// Compares relocations contained with a certain data range.
/// The ObjDataDiffKind for each diff will either be `None`` (if the relocation matches),
/// or `Replace` (if a relocation was changed, added, or removed).
/// `Insert` and `Delete` are not used when a relocation is added or removed to avoid confusing diffs
/// where it looks like the bytes themselves were changed but actually only the relocations changed.
fn diff_data_relocs_for_range(
left_obj: &ObjInfo,
right_obj: &ObjInfo,
left: &ObjSection,
right: &ObjSection,
left_range: Range<usize>,
right_range: Range<usize>,
) -> Vec<(ObjDataDiffKind, Option<ObjReloc>, Option<ObjReloc>)> {
let mut diffs = Vec::new();
for left_reloc in left.relocations.iter() {
if !left_range.contains(&(left_reloc.address as usize)) {
continue;
}
let left_offset = left_reloc.address as usize - left_range.start;
let Some(right_reloc) = right.relocations.iter().find(|r| {
if !right_range.contains(&(r.address as usize)) {
return false;
}
let right_offset = r.address as usize - right_range.start;
right_offset == left_offset
}) else {
diffs.push((ObjDataDiffKind::Delete, Some(left_reloc.clone()), None));
continue;
};
if reloc_eq(left_obj, right_obj, left_reloc, right_reloc) {
diffs.push((
ObjDataDiffKind::None,
Some(left_reloc.clone()),
Some(right_reloc.clone()),
));
} else {
diffs.push((
ObjDataDiffKind::Replace,
Some(left_reloc.clone()),
Some(right_reloc.clone()),
));
}
}
for right_reloc in right.relocations.iter() {
if !right_range.contains(&(right_reloc.address as usize)) {
continue;
}
let right_offset = right_reloc.address as usize - right_range.start;
let Some(_) = left.relocations.iter().find(|r| {
if !left_range.contains(&(r.address as usize)) {
return false;
}
let left_offset = r.address as usize - left_range.start;
left_offset == right_offset
}) else {
diffs.push((ObjDataDiffKind::Insert, None, Some(right_reloc.clone())));
continue;
};
// No need to check the cases for relocations being deleted or matching again.
// They were already handled in the loop over the left relocs.
}
diffs
}
/// Compare the data sections of two object files.
pub fn diff_data_section(
left_obj: &ObjInfo,
right_obj: &ObjInfo,
left: &ObjSection,
right: &ObjSection,
left_section_diff: &ObjSectionDiff,
@@ -227,44 +121,16 @@ pub fn diff_data_section(
}
}
let mut left_reloc_diffs = Vec::new();
let mut right_reloc_diffs = Vec::new();
for (diff_kind, left_reloc, right_reloc) in diff_data_relocs_for_range(
left_obj,
right_obj,
left,
right,
0..left_max as usize,
0..right_max as usize,
) {
if let Some(left_reloc) = left_reloc {
let len = left_obj.arch.get_reloc_byte_size(left_reloc.flags);
let range = left_reloc.address as usize..left_reloc.address as usize + len;
left_reloc_diffs.push(ObjDataRelocDiff { reloc: left_reloc, kind: diff_kind, range });
}
if let Some(right_reloc) = right_reloc {
let len = right_obj.arch.get_reloc_byte_size(right_reloc.flags);
let range = right_reloc.address as usize..right_reloc.address as usize + len;
right_reloc_diffs.push(ObjDataRelocDiff { reloc: right_reloc, kind: diff_kind, range });
}
}
let (mut left_section_diff, mut right_section_diff) =
diff_generic_section(left, right, left_section_diff, right_section_diff)?;
let all_left_relocs_match = left_reloc_diffs.iter().all(|d| d.kind == ObjDataDiffKind::None);
left_section_diff.data_diff = left_diff;
right_section_diff.data_diff = right_diff;
left_section_diff.reloc_diff = left_reloc_diffs;
right_section_diff.reloc_diff = right_reloc_diffs;
if all_left_relocs_match {
// Use the highest match percent between two options:
// - Left symbols matching right symbols by name
// - Diff of the data itself
// We only do this when all relocations on the left side match.
if left_section_diff.match_percent.unwrap_or(-1.0) < match_percent {
left_section_diff.match_percent = Some(match_percent);
right_section_diff.match_percent = Some(match_percent);
}
// Use the highest match percent between two options:
// - Left symbols matching right symbols by name
// - Diff of the data itself
if left_section_diff.match_percent.unwrap_or(-1.0) < match_percent {
left_section_diff.match_percent = Some(match_percent);
right_section_diff.match_percent = Some(match_percent);
}
Ok((left_section_diff, right_section_diff))
}
@@ -281,54 +147,13 @@ pub fn diff_data_symbol(
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_range = left_symbol.section_address as usize
..(left_symbol.section_address + left_symbol.size) as usize;
let right_range = right_symbol.section_address as usize
..(right_symbol.section_address + right_symbol.size) as usize;
let left_data = &left_section.data[left_range.clone()];
let right_data = &right_section.data[right_range.clone()];
let reloc_diffs = diff_data_relocs_for_range(
left_obj,
right_obj,
left_section,
right_section,
left_range,
right_range,
);
let left_data = &left_section.data[left_symbol.section_address as usize
..(left_symbol.section_address + left_symbol.size) as usize];
let right_data = &right_section.data[right_symbol.section_address as usize
..(right_symbol.section_address + right_symbol.size) as usize];
let ops = capture_diff_slices_deadline(Algorithm::Patience, left_data, right_data, None);
let bytes_match_ratio = get_diff_ratio(&ops, left_data.len(), right_data.len());
let mut match_ratio = bytes_match_ratio;
if !reloc_diffs.is_empty() {
let mut total_reloc_bytes = 0;
let mut matching_reloc_bytes = 0;
for (diff_kind, left_reloc, right_reloc) in reloc_diffs {
let reloc_diff_len = match (left_reloc, right_reloc) {
(None, None) => unreachable!(),
(None, Some(right_reloc)) => right_obj.arch.get_reloc_byte_size(right_reloc.flags),
(Some(left_reloc), _) => left_obj.arch.get_reloc_byte_size(left_reloc.flags),
};
total_reloc_bytes += reloc_diff_len;
if diff_kind == ObjDataDiffKind::None {
matching_reloc_bytes += reloc_diff_len;
}
}
if total_reloc_bytes > 0 {
let relocs_match_ratio = matching_reloc_bytes as f32 / total_reloc_bytes as f32;
// Adjust the overall match ratio to include relocation differences.
// We calculate it so that bytes that contain a relocation are counted twice: once for the
// byte's raw value, and once for its relocation.
// e.g. An 8 byte symbol that has 8 matching raw bytes and a single 4 byte relocation that
// doesn't match would show as 66% (weighted average of 100% and 0%).
match_ratio = ((bytes_match_ratio * (left_data.len() as f32))
+ (relocs_match_ratio * total_reloc_bytes as f32))
/ (left_data.len() + total_reloc_bytes) as f32;
}
}
let match_percent = match_ratio * 100.0;
let match_percent = get_diff_ratio(&ops, left_data.len(), right_data.len()) * 100.0;
Ok((
ObjSymbolDiff {
@@ -365,18 +190,8 @@ pub fn diff_generic_section(
/ left.size as f32
};
Ok((
ObjSectionDiff {
symbols: vec![],
data_diff: vec![],
reloc_diff: vec![],
match_percent: Some(match_percent),
},
ObjSectionDiff {
symbols: vec![],
data_diff: vec![],
reloc_diff: vec![],
match_percent: Some(match_percent),
},
ObjSectionDiff { symbols: vec![], data_diff: vec![], match_percent: Some(match_percent) },
ObjSectionDiff { symbols: vec![], data_diff: vec![], match_percent: Some(match_percent) },
))
}
@@ -401,17 +216,7 @@ pub fn diff_bss_section(
}
Ok((
ObjSectionDiff {
symbols: vec![],
data_diff: vec![],
reloc_diff: vec![],
match_percent: Some(match_percent),
},
ObjSectionDiff {
symbols: vec![],
data_diff: vec![],
reloc_diff: vec![],
match_percent: Some(match_percent),
},
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,4 +1,4 @@
use std::{collections::HashSet, ops::Range};
use std::collections::HashSet;
use anyhow::Result;
@@ -11,16 +11,195 @@ use crate::{
diff_generic_section, no_diff_symbol,
},
},
obj::{
ObjInfo, ObjIns, ObjReloc, ObjSection, ObjSectionKind, ObjSymbol, SymbolRef, SECTION_COMMON,
},
obj::{ObjInfo, ObjIns, ObjSection, ObjSectionKind, ObjSymbol, SymbolRef, SECTION_COMMON},
};
pub mod code;
pub mod data;
pub mod display;
include!(concat!(env!("OUT_DIR"), "/config.gen.rs"));
#[derive(
Debug,
Copy,
Clone,
Default,
Eq,
PartialEq,
serde::Deserialize,
serde::Serialize,
strum::VariantArray,
strum::EnumMessage,
)]
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
pub enum X86Formatter {
#[default]
#[strum(message = "Intel (default)")]
Intel,
#[strum(message = "AT&T")]
Gas,
#[strum(message = "NASM")]
Nasm,
#[strum(message = "MASM")]
Masm,
}
#[derive(
Debug,
Copy,
Clone,
Default,
Eq,
PartialEq,
serde::Deserialize,
serde::Serialize,
strum::VariantArray,
strum::EnumMessage,
)]
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
pub enum MipsAbi {
#[default]
#[strum(message = "Auto (default)")]
Auto,
#[strum(message = "O32")]
O32,
#[strum(message = "N32")]
N32,
#[strum(message = "N64")]
N64,
}
#[derive(
Debug,
Copy,
Clone,
Default,
Eq,
PartialEq,
serde::Deserialize,
serde::Serialize,
strum::VariantArray,
strum::EnumMessage,
)]
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
pub enum MipsInstrCategory {
#[default]
#[strum(message = "Auto (default)")]
Auto,
#[strum(message = "CPU")]
Cpu,
#[strum(message = "RSP (N64)")]
Rsp,
#[strum(message = "R3000 GTE (PS1)")]
R3000Gte,
#[strum(message = "R4000 ALLEGREX (PSP)")]
R4000Allegrex,
#[strum(message = "R5900 EE (PS2)")]
R5900,
}
#[derive(
Debug,
Copy,
Clone,
Default,
Eq,
PartialEq,
serde::Deserialize,
serde::Serialize,
strum::VariantArray,
strum::EnumMessage,
)]
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
pub enum ArmArchVersion {
#[default]
#[strum(message = "Auto (default)")]
Auto,
#[strum(message = "ARMv4T (GBA)")]
V4T,
#[strum(message = "ARMv5TE (DS)")]
V5TE,
#[strum(message = "ARMv6K (3DS)")]
V6K,
}
#[derive(
Debug,
Copy,
Clone,
Default,
Eq,
PartialEq,
serde::Deserialize,
serde::Serialize,
strum::VariantArray,
strum::EnumMessage,
)]
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
pub enum ArmR9Usage {
#[default]
#[strum(
message = "R9 or V6 (default)",
detailed_message = "Use R9 as a general-purpose register."
)]
GeneralPurpose,
#[strum(
message = "SB (static base)",
detailed_message = "Used for position-independent data (PID)."
)]
Sb,
#[strum(message = "TR (TLS register)", detailed_message = "Used for thread-local storage.")]
Tr,
}
#[inline]
const fn default_true() -> bool { true }
#[derive(Debug, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
#[cfg_attr(feature = "wasm", tsify(from_wasm_abi))]
#[serde(default)]
pub struct DiffObjConfig {
pub relax_reloc_diffs: bool,
#[serde(default = "default_true")]
pub space_between_args: bool,
pub combine_data_sections: bool,
#[serde(default)]
pub symbol_mappings: MappingConfig,
// x86
pub x86_formatter: X86Formatter,
// MIPS
pub mips_abi: MipsAbi,
pub mips_instr_category: MipsInstrCategory,
// ARM
pub arm_arch_version: ArmArchVersion,
pub arm_unified_syntax: bool,
pub arm_av_registers: bool,
pub arm_r9_usage: ArmR9Usage,
pub arm_sl_usage: bool,
pub arm_fp_usage: bool,
pub arm_ip_usage: bool,
}
impl Default for DiffObjConfig {
fn default() -> Self {
Self {
relax_reloc_diffs: false,
space_between_args: true,
combine_data_sections: false,
symbol_mappings: Default::default(),
x86_formatter: Default::default(),
mips_abi: Default::default(),
mips_instr_category: Default::default(),
arm_arch_version: Default::default(),
arm_unified_syntax: true,
arm_av_registers: false,
arm_r9_usage: Default::default(),
arm_sl_usage: false,
arm_fp_usage: false,
arm_ip_usage: false,
}
}
}
impl DiffObjConfig {
pub fn separator(&self) -> &'static str {
@@ -36,7 +215,6 @@ impl DiffObjConfig {
pub struct ObjSectionDiff {
pub symbols: Vec<ObjSymbolDiff>,
pub data_diff: Vec<ObjDataDiff>,
pub reloc_diff: Vec<ObjDataRelocDiff>,
pub match_percent: Option<f32>,
}
@@ -44,7 +222,6 @@ impl ObjSectionDiff {
fn merge(&mut self, other: ObjSectionDiff) {
// symbols ignored
self.data_diff = other.data_diff;
self.reloc_diff = other.reloc_diff;
self.match_percent = other.match_percent;
}
}
@@ -91,13 +268,6 @@ pub struct ObjDataDiff {
pub symbol: String,
}
#[derive(Debug, Clone)]
pub struct ObjDataRelocDiff {
pub reloc: ObjReloc,
pub kind: ObjDataDiffKind,
pub range: Range<usize>,
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
pub enum ObjDataDiffKind {
#[default]
@@ -165,7 +335,6 @@ impl ObjDiff {
len: section.data.len(),
symbol: section.name.clone(),
}],
reloc_diff: vec![],
match_percent: None,
});
}
@@ -217,13 +386,12 @@ pub struct DiffObjsResult {
}
pub fn diff_objs(
diff_config: &DiffObjConfig,
mapping_config: &MappingConfig,
config: &DiffObjConfig,
left: Option<&ObjInfo>,
right: Option<&ObjInfo>,
prev: Option<&ObjInfo>,
) -> Result<DiffObjsResult> {
let symbol_matches = matching_symbols(left, right, prev, mapping_config)?;
let symbol_matches = matching_symbols(left, right, prev, &config.symbol_mappings)?;
let section_matches = matching_sections(left, right)?;
let mut left = left.map(|p| (p, ObjDiff::new_from_obj(p)));
let mut right = right.map(|p| (p, ObjDiff::new_from_obj(p)));
@@ -241,10 +409,8 @@ pub fn diff_objs(
let (right_obj, right_out) = right.as_mut().unwrap();
match section_kind {
ObjSectionKind::Code => {
let left_code =
process_code_symbol(left_obj, left_symbol_ref, diff_config)?;
let right_code =
process_code_symbol(right_obj, right_symbol_ref, diff_config)?;
let left_code = process_code_symbol(left_obj, left_symbol_ref, config)?;
let right_code = process_code_symbol(right_obj, right_symbol_ref, config)?;
let (left_diff, right_diff) = diff_code(
left_obj,
right_obj,
@@ -252,15 +418,14 @@ pub fn diff_objs(
&right_code,
left_symbol_ref,
right_symbol_ref,
diff_config,
config,
)?;
*left_out.symbol_diff_mut(left_symbol_ref) = left_diff;
*right_out.symbol_diff_mut(right_symbol_ref) = right_diff;
if let Some(prev_symbol_ref) = prev_symbol_ref {
let (prev_obj, prev_out) = prev.as_mut().unwrap();
let prev_code =
process_code_symbol(prev_obj, prev_symbol_ref, diff_config)?;
let prev_code = process_code_symbol(prev_obj, prev_symbol_ref, config)?;
let (_, prev_diff) = diff_code(
left_obj,
right_obj,
@@ -268,7 +433,7 @@ pub fn diff_objs(
&prev_code,
right_symbol_ref,
prev_symbol_ref,
diff_config,
config,
)?;
*prev_out.symbol_diff_mut(prev_symbol_ref) = prev_diff;
}
@@ -299,7 +464,7 @@ pub fn diff_objs(
let (left_obj, left_out) = left.as_mut().unwrap();
match section_kind {
ObjSectionKind::Code => {
let code = process_code_symbol(left_obj, left_symbol_ref, diff_config)?;
let code = process_code_symbol(left_obj, left_symbol_ref, config)?;
*left_out.symbol_diff_mut(left_symbol_ref) =
no_diff_code(&code, left_symbol_ref)?;
}
@@ -313,7 +478,7 @@ pub fn diff_objs(
let (right_obj, right_out) = right.as_mut().unwrap();
match section_kind {
ObjSectionKind::Code => {
let code = process_code_symbol(right_obj, right_symbol_ref, diff_config)?;
let code = process_code_symbol(right_obj, right_symbol_ref, config)?;
*right_out.symbol_diff_mut(right_symbol_ref) =
no_diff_code(&code, right_symbol_ref)?;
}
@@ -357,8 +522,6 @@ pub fn diff_objs(
let left_section_diff = left_out.section_diff(left_section_idx);
let right_section_diff = right_out.section_diff(right_section_idx);
let (left_diff, right_diff) = diff_data_section(
left_obj,
right_obj,
left_section,
right_section,
left_section_diff,
@@ -386,11 +549,11 @@ pub fn diff_objs(
if let (Some((right_obj, right_out)), Some((left_obj, left_out))) =
(right.as_mut(), left.as_mut())
{
if let Some(right_name) = &mapping_config.selecting_left {
generate_mapping_symbols(right_obj, right_name, left_obj, left_out, diff_config)?;
if let Some(right_name) = &config.symbol_mappings.selecting_left {
generate_mapping_symbols(right_obj, right_name, left_obj, left_out, config)?;
}
if let Some(left_name) = &mapping_config.selecting_right {
generate_mapping_symbols(left_obj, left_name, right_obj, right_out, diff_config)?;
if let Some(left_name) = &config.symbol_mappings.selecting_right {
generate_mapping_symbols(left_obj, left_name, right_obj, right_out, config)?;
}
}
@@ -473,9 +636,7 @@ struct SectionMatch {
section_kind: ObjSectionKind,
}
#[derive(Debug, Clone, Default, serde::Deserialize, serde::Serialize)]
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify), tsify(from_wasm_abi))]
#[serde(default)]
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, serde::Deserialize, serde::Serialize)]
pub struct MappingConfig {
/// Manual symbol mappings
pub mappings: SymbolMappings,

View File

@@ -1,50 +0,0 @@
use std::{sync::mpsc::Receiver, task::Waker};
use anyhow::{Context, Result};
use self_update::{
cargo_crate_version,
update::{Release, ReleaseUpdate},
};
use crate::jobs::{start_job, update_status, Job, JobContext, JobResult, JobState};
pub struct CheckUpdateConfig {
pub build_updater: fn() -> Result<Box<dyn ReleaseUpdate>>,
pub bin_names: Vec<String>,
}
pub struct CheckUpdateResult {
pub update_available: bool,
pub latest_release: Release,
pub found_binary: Option<String>,
}
fn run_check_update(
context: &JobContext,
cancel: Receiver<()>,
config: CheckUpdateConfig,
) -> Result<Box<CheckUpdateResult>> {
update_status(context, "Fetching latest release".to_string(), 0, 1, &cancel)?;
let updater = (config.build_updater)().context("Failed to create release updater")?;
let latest_release = updater.get_latest_release()?;
let update_available =
self_update::version::bump_is_greater(cargo_crate_version!(), &latest_release.version)?;
// Find the binary name in the release assets
let mut found_binary = None;
for bin_name in &config.bin_names {
if latest_release.assets.iter().any(|a| &a.name == bin_name) {
found_binary = Some(bin_name.clone());
break;
}
}
update_status(context, "Complete".to_string(), 1, 1, &cancel)?;
Ok(Box::new(CheckUpdateResult { update_available, latest_release, found_binary }))
}
pub fn start_check_update(waker: Waker, config: CheckUpdateConfig) -> JobState {
start_job(waker, "Check for updates", Job::CheckUpdate, move |context, cancel| {
run_check_update(&context, cancel, config)
.map(|result| JobResult::CheckUpdate(Some(result)))
})
}

View File

@@ -1,195 +0,0 @@
use std::{path::PathBuf, sync::mpsc::Receiver, task::Waker};
use anyhow::{anyhow, Error, Result};
use time::OffsetDateTime;
use crate::{
build::{run_make, BuildConfig, BuildStatus},
diff::{diff_objs, DiffObjConfig, MappingConfig, ObjDiff},
jobs::{start_job, update_status, Job, JobContext, JobResult, JobState},
obj::{read, ObjInfo},
};
pub struct ObjDiffConfig {
pub build_config: BuildConfig,
pub build_base: bool,
pub build_target: bool,
pub target_path: Option<PathBuf>,
pub base_path: Option<PathBuf>,
pub diff_obj_config: DiffObjConfig,
pub mapping_config: MappingConfig,
}
pub struct ObjDiffResult {
pub first_status: BuildStatus,
pub second_status: BuildStatus,
pub first_obj: Option<(ObjInfo, ObjDiff)>,
pub second_obj: Option<(ObjInfo, ObjDiff)>,
pub time: OffsetDateTime,
}
fn run_build(
context: &JobContext,
cancel: Receiver<()>,
config: ObjDiffConfig,
) -> Result<Box<ObjDiffResult>> {
let mut target_path_rel = None;
let mut base_path_rel = None;
if config.build_target || config.build_base {
let project_dir = config
.build_config
.project_dir
.as_ref()
.ok_or_else(|| Error::msg("Missing project dir"))?;
if let Some(target_path) = &config.target_path {
target_path_rel = Some(target_path.strip_prefix(project_dir).map_err(|_| {
anyhow!(
"Target path '{}' doesn't begin with '{}'",
target_path.display(),
project_dir.display()
)
})?);
}
if let Some(base_path) = &config.base_path {
base_path_rel = Some(base_path.strip_prefix(project_dir).map_err(|_| {
anyhow!(
"Base path '{}' doesn't begin with '{}'",
base_path.display(),
project_dir.display()
)
})?);
};
}
let mut total = 1;
if config.build_target && target_path_rel.is_some() {
total += 1;
}
if config.build_base && base_path_rel.is_some() {
total += 1;
}
if config.target_path.is_some() {
total += 1;
}
if config.base_path.is_some() {
total += 1;
}
let mut step_idx = 0;
let mut first_status = match target_path_rel {
Some(target_path_rel) if config.build_target => {
update_status(
context,
format!("Building target {}", target_path_rel.display()),
step_idx,
total,
&cancel,
)?;
step_idx += 1;
run_make(&config.build_config, target_path_rel)
}
_ => BuildStatus::default(),
};
let mut second_status = match base_path_rel {
Some(base_path_rel) if config.build_base => {
update_status(
context,
format!("Building base {}", base_path_rel.display()),
step_idx,
total,
&cancel,
)?;
step_idx += 1;
run_make(&config.build_config, base_path_rel)
}
_ => BuildStatus::default(),
};
let time = OffsetDateTime::now_utc();
let first_obj = match &config.target_path {
Some(target_path) if first_status.success => {
update_status(
context,
format!("Loading target {}", target_path.display()),
step_idx,
total,
&cancel,
)?;
step_idx += 1;
match read::read(target_path, &config.diff_obj_config) {
Ok(obj) => Some(obj),
Err(e) => {
first_status = BuildStatus {
success: false,
stdout: format!("Loading object '{}'", target_path.display()),
stderr: format!("{:#}", e),
..Default::default()
};
None
}
}
}
Some(_) => {
step_idx += 1;
None
}
_ => None,
};
let second_obj = match &config.base_path {
Some(base_path) if second_status.success => {
update_status(
context,
format!("Loading base {}", base_path.display()),
step_idx,
total,
&cancel,
)?;
step_idx += 1;
match read::read(base_path, &config.diff_obj_config) {
Ok(obj) => Some(obj),
Err(e) => {
second_status = BuildStatus {
success: false,
stdout: format!("Loading object '{}'", base_path.display()),
stderr: format!("{:#}", e),
..Default::default()
};
None
}
}
}
Some(_) => {
step_idx += 1;
None
}
_ => None,
};
update_status(context, "Performing diff".to_string(), step_idx, total, &cancel)?;
step_idx += 1;
let result = diff_objs(
&config.diff_obj_config,
&config.mapping_config,
first_obj.as_ref(),
second_obj.as_ref(),
None,
)?;
update_status(context, "Complete".to_string(), step_idx, total, &cancel)?;
Ok(Box::new(ObjDiffResult {
first_status,
second_status,
first_obj: first_obj.and_then(|o| result.left.map(|d| (o, d))),
second_obj: second_obj.and_then(|o| result.right.map(|d| (o, d))),
time,
}))
}
pub fn start_build(waker: Waker, config: ObjDiffConfig) -> JobState {
start_job(waker, "Build", Job::ObjDiff, move |context, cancel| {
run_build(&context, cancel, config).map(|result| JobResult::ObjDiff(Some(result)))
})
}

View File

@@ -2,14 +2,10 @@
pub mod arch;
#[cfg(feature = "bindings")]
pub mod bindings;
#[cfg(feature = "build")]
pub mod build;
#[cfg(feature = "config")]
pub mod config;
#[cfg(feature = "any-arch")]
pub mod diff;
#[cfg(feature = "build")]
pub mod jobs;
#[cfg(feature = "any-arch")]
pub mod obj;
#[cfg(feature = "any-arch")]

View File

@@ -28,7 +28,7 @@ flags! {
HasExtra,
}
}
#[derive(Debug, Copy, Clone, Default, PartialEq)]
#[derive(Debug, Copy, Clone, Default)]
pub struct ObjSymbolFlagSet(pub FlagSet<ObjSymbolFlags>);
#[derive(Debug, Clone)]
@@ -132,7 +132,7 @@ pub enum ObjSymbolKind {
Section,
}
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone)]
pub struct ObjSymbol {
pub name: String,
pub demangled_name: Option<String>,
@@ -161,7 +161,7 @@ pub struct ObjInfo {
pub split_meta: Option<SplitMeta>,
}
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone)]
pub struct ObjReloc {
pub flags: RelocationFlags,
pub address: u64,

View File

@@ -29,15 +29,16 @@ bytes = "1.9"
cfg-if = "1.0"
const_format = "0.2"
cwdemangle = "1.0"
cwextab = "1.0"
cwextab = "1.0.2"
dirs = "5.0"
egui = "0.30"
egui_extras = "0.30"
egui = "0.29"
egui_extras = "0.29"
filetime = "0.2"
float-ord = "0.3"
font-kit = "0.14"
globset = { version = "0.4", features = ["serde1"] }
log = "0.4"
notify = { git = "https://github.com/notify-rs/notify", rev = "128bf6230c03d39dbb7f301ff7b20e594e34c3a2" }
objdiff-core = { path = "../objdiff-core", features = ["all"] }
open = "5.3"
png = "0.17"
@@ -50,11 +51,12 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
shell-escape = "0.1"
strum = { version = "0.26", features = ["derive"] }
tempfile = "3.14"
time = { version = "0.3", features = ["formatting", "local-offset"] }
# Keep version in sync with egui
[dependencies.eframe]
version = "0.30"
version = "0.29"
features = [
"default_fonts",
"persistence",
@@ -65,7 +67,7 @@ default-features = false
# Keep version in sync with eframe
[dependencies.wgpu]
version = "23.0"
version = "22.1"
features = [
"dx12",
"metal",
@@ -74,7 +76,18 @@ features = [
optional = true
default-features = false
# For Linux static binaries, use rustls
[target.'cfg(target_os = "linux")'.dependencies]
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "multipart", "rustls-tls"] }
self_update = { version = "0.41", default-features = false, features = ["rustls"] }
# For all other platforms, use native TLS
[target.'cfg(not(target_os = "linux"))'.dependencies]
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "multipart", "default-tls"] }
self_update = "0.41"
[target.'cfg(windows)'.dependencies]
path-slash = "0.2"
winapi = "0.3"
[target.'cfg(unix)'.dependencies]
@@ -93,4 +106,4 @@ tracing-wasm = "0.2"
anyhow = "1.0"
[target.'cfg(windows)'.build-dependencies]
tauri-winres = "0.2"
tauri-winres = "0.1"

View File

@@ -11,27 +11,28 @@ use std::{
};
use filetime::FileTime;
use globset::Glob;
use globset::{Glob, GlobSet};
use notify::{RecursiveMode, Watcher};
use objdiff_core::{
build::watcher::{create_watcher, Watcher},
config::{
build_globset, default_watch_patterns, save_project_config, ProjectConfig,
ProjectConfigInfo, ProjectObject, ScratchConfig, SymbolMappings, DEFAULT_WATCH_PATTERNS,
build_globset, save_project_config, ProjectConfig, ProjectConfigInfo, ProjectObject,
ScratchConfig, SymbolMappings, DEFAULT_WATCH_PATTERNS,
},
diff::DiffObjConfig,
jobs::{Job, JobQueue, JobResult},
};
use time::UtcOffset;
use crate::{
app_config::{deserialize_config, AppConfigVersion},
config::{load_project_config, ProjectObjectNode},
jobs::{create_objdiff_config, egui_waker, start_build},
jobs::{
objdiff::{start_build, ObjDiffConfig},
Job, JobQueue, JobResult, JobStatus,
},
views::{
appearance::{appearance_window, Appearance},
config::{
arch_config_window, config_ui, general_config_ui, project_window, ConfigViewState,
CONFIG_DISABLED_TEXT,
arch_config_window, config_ui, project_window, ConfigViewState, CONFIG_DISABLED_TEXT,
},
data_diff::data_diff_ui,
debug::debug_window,
@@ -120,6 +121,11 @@ impl From<&ProjectObject> for ObjectConfig {
#[inline]
fn bool_true() -> bool { true }
#[inline]
fn default_watch_patterns() -> Vec<Glob> {
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect()
}
pub struct AppState {
pub config: AppConfig,
pub objects: Vec<ProjectObject>,
@@ -294,7 +300,7 @@ impl AppState {
let Some(object) = self.config.selected_obj.as_mut() else {
return;
};
object.symbol_mappings.retain(|_, r| r != right);
object.symbol_mappings.remove_by_right(right);
self.selecting_left = Some(right.to_string());
self.queue_reload = true;
self.save_config();
@@ -304,7 +310,7 @@ impl AppState {
let Some(object) = self.config.selected_obj.as_mut() else {
return;
};
object.symbol_mappings.retain(|l, _| l != left);
object.symbol_mappings.remove_by_left(left);
self.selecting_right = Some(left.to_string());
self.queue_reload = true;
self.save_config();
@@ -317,8 +323,10 @@ impl AppState {
};
self.selecting_left = None;
self.selecting_right = None;
object.symbol_mappings.retain(|l, r| l != &left && r != &right);
if left != right {
if left == right {
object.symbol_mappings.remove_by_left(&left);
object.symbol_mappings.remove_by_right(&right);
} else {
object.symbol_mappings.insert(left.clone(), right.clone());
}
self.queue_reload = true;
@@ -391,7 +399,7 @@ pub struct App {
view_state: ViewState,
state: AppStateRef,
modified: Arc<AtomicBool>,
watcher: Option<Watcher>,
watcher: Option<notify::RecommendedWatcher>,
app_path: Option<PathBuf>,
relaunch_path: Rc<Mutex<Option<PathBuf>>>,
should_relaunch: bool,
@@ -466,27 +474,59 @@ impl App {
let ViewState { jobs, diff_state, config_state, .. } = &mut self.view_state;
jobs.collect_results();
jobs.results.retain(|result| match result {
JobResult::Update(state) => {
if let Ok(mut guard) = self.relaunch_path.lock() {
*guard = Some(state.exe_path.clone());
self.should_relaunch = true;
let mut results = vec![];
for (job, result) in jobs.iter_finished() {
match result {
Ok(result) => {
log::info!("Job {} finished", job.id);
match result {
JobResult::None => {
if let Some(err) = &job.context.status.read().unwrap().error {
log::error!("{:?}", err);
}
}
JobResult::Update(state) => {
if let Ok(mut guard) = self.relaunch_path.lock() {
*guard = Some(state.exe_path);
self.should_relaunch = true;
}
}
_ => results.push(result),
}
}
Err(err) => {
let err = if let Some(msg) = err.downcast_ref::<&'static str>() {
anyhow::Error::msg(*msg)
} else if let Some(msg) = err.downcast_ref::<String>() {
anyhow::Error::msg(msg.clone())
} else {
anyhow::Error::msg("Thread panicked")
};
let result = job.context.status.write();
if let Ok(mut guard) = result {
guard.error = Some(err);
} else {
drop(result);
job.context.status = Arc::new(RwLock::new(JobStatus {
title: "Error".to_string(),
progress_percent: 0.0,
progress_items: None,
status: String::new(),
error: Some(err),
}));
}
}
false
}
_ => true,
});
}
jobs.results.append(&mut results);
jobs.clear_finished();
diff_state.pre_update(jobs, &self.state);
config_state.pre_update(jobs, &self.state);
debug_assert!(jobs.results.is_empty());
}
fn post_update(&mut self, ctx: &egui::Context, action: Option<DiffViewAction>) {
if action.is_some() {
ctx.request_repaint();
}
self.appearance.post_update(ctx);
let ViewState { jobs, diff_state, config_state, graphics_state, .. } = &mut self.view_state;
@@ -532,7 +572,7 @@ impl App {
match build_globset(&state.config.watch_patterns)
.map_err(anyhow::Error::new)
.and_then(|globset| {
create_watcher(self.modified.clone(), project_dir, globset, egui_waker(ctx))
create_watcher(ctx.clone(), self.modified.clone(), project_dir, globset)
.map_err(anyhow::Error::new)
}) {
Ok(watcher) => self.watcher = Some(watcher),
@@ -579,15 +619,15 @@ impl App {
&& state.config.selected_obj.is_some()
&& !jobs.is_running(Job::ObjDiff)
{
start_build(ctx, jobs, create_objdiff_config(state));
jobs.push(start_build(ctx, ObjDiffConfig::from_state(state)));
state.queue_build = false;
state.queue_reload = false;
} else if state.queue_reload && !jobs.is_running(Job::ObjDiff) {
let mut diff_config = create_objdiff_config(state);
let mut diff_config = ObjDiffConfig::from_state(state);
// Don't build, just reload the current files
diff_config.build_base = false;
diff_config.build_target = false;
start_build(ctx, jobs, diff_config);
jobs.push(start_build(ctx, diff_config));
state.queue_reload = false;
}
@@ -727,9 +767,37 @@ impl eframe::App for App {
&mut diff_state.symbol_state.show_hidden_symbols,
"Show hidden symbols",
);
ui.separator();
general_config_ui(ui, &mut state);
ui.separator();
if ui
.checkbox(
&mut state.config.diff_obj_config.relax_reloc_diffs,
"Relax relocation diffs",
)
.on_hover_text(
"Ignores differences in relocation targets. (Address, name, etc)",
)
.changed()
{
state.queue_reload = true;
}
if ui
.checkbox(
&mut state.config.diff_obj_config.space_between_args,
"Space between args",
)
.changed()
{
state.queue_reload = true;
}
if ui
.checkbox(
&mut state.config.diff_obj_config.combine_data_sections,
"Combine data sections",
)
.on_hover_text("Combines data sections with equal names.")
.changed()
{
state.queue_reload = true;
}
if ui.button("Clear custom symbol mappings").clicked() {
state.clear_mappings();
diff_state.post_build_nav = Some(DiffViewNavigation::symbol_diff());
@@ -786,6 +854,40 @@ impl eframe::App for App {
}
}
fn create_watcher(
ctx: egui::Context,
modified: Arc<AtomicBool>,
project_dir: &Path,
patterns: GlobSet,
) -> notify::Result<notify::RecommendedWatcher> {
let base_dir = project_dir.to_owned();
let mut watcher =
notify::recommended_watcher(move |res: notify::Result<notify::Event>| match res {
Ok(event) => {
if matches!(
event.kind,
notify::EventKind::Modify(..)
| notify::EventKind::Create(..)
| notify::EventKind::Remove(..)
) {
for path in &event.paths {
let Ok(path) = path.strip_prefix(&base_dir) else {
continue;
};
if patterns.is_match(path) {
log::info!("File modified: {}", path.display());
modified.store(true, Ordering::Relaxed);
ctx.request_repaint();
}
}
}
}
Err(e) => log::error!("watch error: {e:?}"),
})?;
watcher.watch(project_dir, RecursiveMode::Recursive)?;
Ok(watcher)
}
#[inline]
fn file_modified(path: &Path, last_ts: FileTime) -> bool {
if let Ok(metadata) = fs::metadata(path) {

View File

@@ -3,11 +3,8 @@ use std::path::PathBuf;
use eframe::Storage;
use globset::Glob;
use objdiff_core::{
config::{ScratchConfig, SymbolMappings},
diff::{
ArmArchVersion, ArmR9Usage, DiffObjConfig, FunctionRelocDiffs, MipsAbi, MipsInstrCategory,
X86Formatter,
},
config::ScratchConfig,
diff::{ArmArchVersion, ArmR9Usage, DiffObjConfig, MipsAbi, MipsInstrCategory, X86Formatter},
};
use crate::app::{AppConfig, ObjectConfig, CONFIG_KEY};
@@ -18,7 +15,7 @@ pub struct AppConfigVersion {
}
impl Default for AppConfigVersion {
fn default() -> Self { Self { version: 3 } }
fn default() -> Self { Self { version: 2 } }
}
/// Deserialize the AppConfig from storage, handling upgrades from older versions.
@@ -26,8 +23,7 @@ pub fn deserialize_config(storage: &dyn Storage) -> Option<AppConfig> {
let str = storage.get_string(CONFIG_KEY)?;
match ron::from_str::<AppConfigVersion>(&str) {
Ok(version) => match version.version {
3 => from_str::<AppConfig>(&str),
2 => from_str::<AppConfigV2>(&str).map(|c| c.into_config()),
2 => from_str::<AppConfig>(&str),
1 => from_str::<AppConfigV1>(&str).map(|c| c.into_config()),
_ => {
log::warn!("Unknown config version: {}", version.version);
@@ -53,119 +49,6 @@ where T: serde::de::DeserializeOwned {
}
}
#[derive(serde::Deserialize, serde::Serialize)]
pub struct ScratchConfigV2 {
#[serde(default)]
pub platform: Option<String>,
#[serde(default)]
pub compiler: Option<String>,
#[serde(default)]
pub c_flags: Option<String>,
#[serde(default)]
pub ctx_path: Option<PathBuf>,
#[serde(default)]
pub build_ctx: Option<bool>,
#[serde(default)]
pub preset_id: Option<u32>,
}
impl ScratchConfigV2 {
fn into_config(self) -> ScratchConfig {
ScratchConfig {
platform: self.platform,
compiler: self.compiler,
c_flags: self.c_flags,
ctx_path: self.ctx_path,
build_ctx: self.build_ctx,
preset_id: self.preset_id,
}
}
}
#[derive(serde::Deserialize, serde::Serialize)]
pub struct ObjectConfigV2 {
pub name: String,
pub target_path: Option<PathBuf>,
pub base_path: Option<PathBuf>,
pub reverse_fn_order: Option<bool>,
pub complete: Option<bool>,
pub scratch: Option<ScratchConfigV2>,
pub source_path: Option<String>,
#[serde(default)]
pub symbol_mappings: SymbolMappings,
}
impl ObjectConfigV2 {
fn into_config(self) -> ObjectConfig {
ObjectConfig {
name: self.name,
target_path: self.target_path,
base_path: self.base_path,
reverse_fn_order: self.reverse_fn_order,
complete: self.complete,
scratch: self.scratch.map(|scratch| scratch.into_config()),
source_path: self.source_path,
symbol_mappings: self.symbol_mappings,
}
}
}
#[derive(serde::Deserialize, serde::Serialize)]
pub struct AppConfigV2 {
pub version: u32,
#[serde(default)]
pub custom_make: Option<String>,
#[serde(default)]
pub custom_args: Option<Vec<String>>,
#[serde(default)]
pub selected_wsl_distro: Option<String>,
#[serde(default)]
pub project_dir: Option<PathBuf>,
#[serde(default)]
pub target_obj_dir: Option<PathBuf>,
#[serde(default)]
pub base_obj_dir: Option<PathBuf>,
#[serde(default)]
pub selected_obj: Option<ObjectConfigV2>,
#[serde(default = "bool_true")]
pub build_base: bool,
#[serde(default)]
pub build_target: bool,
#[serde(default = "bool_true")]
pub rebuild_on_changes: bool,
#[serde(default)]
pub auto_update_check: bool,
#[serde(default)]
pub watch_patterns: Vec<Glob>,
#[serde(default)]
pub recent_projects: Vec<PathBuf>,
#[serde(default)]
pub diff_obj_config: DiffObjConfigV1,
}
impl AppConfigV2 {
fn into_config(self) -> AppConfig {
log::info!("Upgrading configuration from v2");
AppConfig {
custom_make: self.custom_make,
custom_args: self.custom_args,
selected_wsl_distro: self.selected_wsl_distro,
project_dir: self.project_dir,
target_obj_dir: self.target_obj_dir,
base_obj_dir: self.base_obj_dir,
selected_obj: self.selected_obj.map(|obj| obj.into_config()),
build_base: self.build_base,
build_target: self.build_target,
rebuild_on_changes: self.rebuild_on_changes,
auto_update_check: self.auto_update_check,
watch_patterns: self.watch_patterns,
recent_projects: self.recent_projects,
diff_obj_config: self.diff_obj_config.into_config(),
..Default::default()
}
}
}
#[derive(serde::Deserialize, serde::Serialize)]
pub struct ScratchConfigV1 {
#[serde(default)]
@@ -264,11 +147,7 @@ impl Default for DiffObjConfigV1 {
impl DiffObjConfigV1 {
fn into_config(self) -> DiffObjConfig {
DiffObjConfig {
function_reloc_diffs: if self.relax_reloc_diffs {
FunctionRelocDiffs::None
} else {
FunctionRelocDiffs::default()
},
relax_reloc_diffs: self.relax_reloc_diffs,
space_between_args: self.space_between_args,
combine_data_sections: self.combine_data_sections,
x86_formatter: self.x86_formatter,
@@ -281,6 +160,7 @@ impl DiffObjConfigV1 {
arm_sl_usage: self.arm_sl_usage,
arm_fp_usage: self.arm_fp_usage,
arm_ip_usage: self.arm_ip_usage,
..Default::default()
}
}
}

View File

@@ -99,15 +99,9 @@ pub fn load_project_config(state: &mut AppState) -> Result<()> {
state.config.base_obj_dir = project_config.base_dir.as_deref().map(|p| project_dir.join(p));
state.config.build_base = project_config.build_base.unwrap_or(true);
state.config.build_target = project_config.build_target.unwrap_or(false);
if let Some(watch_patterns) = &project_config.watch_patterns {
state.config.watch_patterns = watch_patterns
.iter()
.map(|s| Glob::new(s))
.collect::<Result<Vec<Glob>, globset::Error>>()?;
} else {
state.config.watch_patterns =
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect();
}
state.config.watch_patterns = project_config.watch_patterns.clone().unwrap_or_else(|| {
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect()
});
state.watcher_change = true;
state.objects = project_config.units.clone().unwrap_or_default();
state.object_nodes = build_nodes(

View File

@@ -96,7 +96,7 @@ pub fn load_font_if_needed(
let default_font = family.handles.get(family.default_index).unwrap();
let default_font_data = load_font(default_font).unwrap();
log::info!("Loaded font family '{}'", family.family_name);
fonts.font_data.insert(default_font_ref.full_name(), Arc::new(default_font_data.font_data));
fonts.font_data.insert(default_font_ref.full_name(), default_font_data.font_data);
fonts
.families
.entry(egui::FontFamily::Name(Arc::from(family.family_name)))

View File

@@ -1,108 +0,0 @@
use egui::{
style::ScrollAnimation, vec2, Context, Key, KeyboardShortcut, Modifiers, PointerButton,
};
fn any_widget_focused(ctx: &Context) -> bool { ctx.memory(|mem| mem.focused().is_some()) }
pub fn enter_pressed(ctx: &Context) -> bool {
if any_widget_focused(ctx) {
return false;
}
ctx.input_mut(|i| {
i.key_pressed(Key::Enter)
|| i.key_pressed(Key::Space)
|| i.pointer.button_pressed(PointerButton::Extra2)
})
}
pub fn back_pressed(ctx: &Context) -> bool {
if any_widget_focused(ctx) {
return false;
}
ctx.input_mut(|i| {
i.key_pressed(Key::Backspace)
|| i.key_pressed(Key::Escape)
|| i.pointer.button_pressed(PointerButton::Extra1)
})
}
pub fn up_pressed(ctx: &Context) -> bool {
if any_widget_focused(ctx) {
return false;
}
ctx.input_mut(|i| i.key_pressed(Key::ArrowUp) || i.key_pressed(Key::W))
}
pub fn down_pressed(ctx: &Context) -> bool {
if any_widget_focused(ctx) {
return false;
}
ctx.input_mut(|i| i.key_pressed(Key::ArrowDown) || i.key_pressed(Key::S))
}
pub fn page_up_pressed(ctx: &Context) -> bool { ctx.input_mut(|i| i.key_pressed(Key::PageUp)) }
pub fn page_down_pressed(ctx: &Context) -> bool { ctx.input_mut(|i| i.key_pressed(Key::PageDown)) }
pub fn home_pressed(ctx: &Context) -> bool { ctx.input_mut(|i| i.key_pressed(Key::Home)) }
pub fn end_pressed(ctx: &Context) -> bool { ctx.input_mut(|i| i.key_pressed(Key::End)) }
pub fn check_scroll_hotkeys(ui: &mut egui::Ui, include_small_increments: bool) {
let ui_height = ui.available_rect_before_wrap().height();
if up_pressed(ui.ctx()) && include_small_increments {
ui.scroll_with_delta_animation(vec2(0.0, ui_height / 10.0), ScrollAnimation::none());
} else if down_pressed(ui.ctx()) && include_small_increments {
ui.scroll_with_delta_animation(vec2(0.0, -ui_height / 10.0), ScrollAnimation::none());
} else if page_up_pressed(ui.ctx()) {
ui.scroll_with_delta_animation(vec2(0.0, ui_height), ScrollAnimation::none());
} else if page_down_pressed(ui.ctx()) {
ui.scroll_with_delta_animation(vec2(0.0, -ui_height), ScrollAnimation::none());
} else if home_pressed(ui.ctx()) {
ui.scroll_with_delta_animation(vec2(0.0, f32::INFINITY), ScrollAnimation::none());
} else if end_pressed(ui.ctx()) {
ui.scroll_with_delta_animation(vec2(0.0, -f32::INFINITY), ScrollAnimation::none());
}
}
pub fn consume_up_key(ctx: &Context) -> bool {
if any_widget_focused(ctx) {
return false;
}
ctx.input_mut(|i| {
i.consume_key(Modifiers::NONE, Key::ArrowUp) || i.consume_key(Modifiers::NONE, Key::W)
})
}
pub fn consume_down_key(ctx: &Context) -> bool {
if any_widget_focused(ctx) {
return false;
}
ctx.input_mut(|i| {
i.consume_key(Modifiers::NONE, Key::ArrowDown) || i.consume_key(Modifiers::NONE, Key::S)
})
}
const OBJECT_FILTER_SHORTCUT: KeyboardShortcut = KeyboardShortcut::new(Modifiers::CTRL, Key::F);
pub fn consume_object_filter_shortcut(ctx: &Context) -> bool {
ctx.input_mut(|i| i.consume_shortcut(&OBJECT_FILTER_SHORTCUT))
}
const SYMBOL_FILTER_SHORTCUT: KeyboardShortcut = KeyboardShortcut::new(Modifiers::CTRL, Key::S);
pub fn consume_symbol_filter_shortcut(ctx: &Context) -> bool {
ctx.input_mut(|i| i.consume_shortcut(&SYMBOL_FILTER_SHORTCUT))
}
const CHANGE_TARGET_SHORTCUT: KeyboardShortcut = KeyboardShortcut::new(Modifiers::CTRL, Key::T);
pub fn consume_change_target_shortcut(ctx: &Context) -> bool {
ctx.input_mut(|i| i.consume_shortcut(&CHANGE_TARGET_SHORTCUT))
}
const CHANGE_BASE_SHORTCUT: KeyboardShortcut = KeyboardShortcut::new(Modifiers::CTRL, Key::B);
pub fn consume_change_base_shortcut(ctx: &Context) -> bool {
ctx.input_mut(|i| i.consume_shortcut(&CHANGE_BASE_SHORTCUT))
}

View File

@@ -1,141 +0,0 @@
use std::{
sync::Arc,
task::{Wake, Waker},
};
use anyhow::{bail, Result};
use jobs::create_scratch;
use objdiff_core::{
build::BuildConfig,
diff::MappingConfig,
jobs,
jobs::{check_update::CheckUpdateConfig, objdiff, update::UpdateConfig, Job, JobQueue},
};
use crate::{
app::{AppConfig, AppState},
update::{build_updater, BIN_NAME_NEW, BIN_NAME_OLD},
};
struct EguiWaker(egui::Context);
impl Wake for EguiWaker {
fn wake(self: Arc<Self>) { self.0.request_repaint(); }
fn wake_by_ref(self: &Arc<Self>) { self.0.request_repaint(); }
}
pub fn egui_waker(ctx: &egui::Context) -> Waker { Waker::from(Arc::new(EguiWaker(ctx.clone()))) }
pub fn is_create_scratch_available(config: &AppConfig) -> bool {
let Some(selected_obj) = &config.selected_obj else {
return false;
};
selected_obj.target_path.is_some() && selected_obj.scratch.is_some()
}
pub fn start_create_scratch(
ctx: &egui::Context,
jobs: &mut JobQueue,
state: &AppState,
function_name: String,
) {
match create_scratch_config(state, function_name) {
Ok(config) => {
jobs.push_once(Job::CreateScratch, || {
create_scratch::start_create_scratch(egui_waker(ctx), config)
});
}
Err(err) => {
log::error!("Failed to create scratch config: {err}");
}
}
}
fn create_scratch_config(
state: &AppState,
function_name: String,
) -> Result<create_scratch::CreateScratchConfig> {
let Some(selected_obj) = &state.config.selected_obj else {
bail!("No object selected");
};
let Some(target_path) = &selected_obj.target_path else {
bail!("No target path for {}", selected_obj.name);
};
let Some(scratch_config) = &selected_obj.scratch else {
bail!("No scratch configuration for {}", selected_obj.name);
};
Ok(create_scratch::CreateScratchConfig {
build_config: BuildConfig::from(&state.config),
context_path: scratch_config.ctx_path.clone(),
build_context: scratch_config.build_ctx.unwrap_or(false),
compiler: scratch_config.compiler.clone().unwrap_or_default(),
platform: scratch_config.platform.clone().unwrap_or_default(),
compiler_flags: scratch_config.c_flags.clone().unwrap_or_default(),
function_name,
target_obj: target_path.to_path_buf(),
preset_id: scratch_config.preset_id,
})
}
impl From<&AppConfig> for BuildConfig {
fn from(config: &AppConfig) -> Self {
Self {
project_dir: config.project_dir.clone(),
custom_make: config.custom_make.clone(),
custom_args: config.custom_args.clone(),
selected_wsl_distro: config.selected_wsl_distro.clone(),
}
}
}
pub fn create_objdiff_config(state: &AppState) -> objdiff::ObjDiffConfig {
objdiff::ObjDiffConfig {
build_config: BuildConfig::from(&state.config),
build_base: state.config.build_base,
build_target: state.config.build_target,
target_path: state
.config
.selected_obj
.as_ref()
.and_then(|obj| obj.target_path.as_ref())
.cloned(),
base_path: state
.config
.selected_obj
.as_ref()
.and_then(|obj| obj.base_path.as_ref())
.cloned(),
diff_obj_config: state.config.diff_obj_config.clone(),
mapping_config: MappingConfig {
mappings: state
.config
.selected_obj
.as_ref()
.map(|obj| &obj.symbol_mappings)
.cloned()
.unwrap_or_default(),
selecting_left: state.selecting_left.clone(),
selecting_right: state.selecting_right.clone(),
},
}
}
pub fn start_build(ctx: &egui::Context, jobs: &mut JobQueue, config: objdiff::ObjDiffConfig) {
jobs.push_once(Job::ObjDiff, || objdiff::start_build(egui_waker(ctx), config));
}
pub fn start_check_update(ctx: &egui::Context, jobs: &mut JobQueue) {
jobs.push_once(Job::Update, || {
jobs::check_update::start_check_update(egui_waker(ctx), CheckUpdateConfig {
build_updater,
bin_names: vec![BIN_NAME_NEW.to_string(), BIN_NAME_OLD.to_string()],
})
});
}
pub fn start_update(ctx: &egui::Context, jobs: &mut JobQueue, bin_name: String) {
jobs.push_once(Job::Update, || {
jobs::update::start_update(egui_waker(ctx), UpdateConfig { build_updater, bin_name })
});
}

View File

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

View File

@@ -1,10 +1,14 @@
use std::{fs, path::PathBuf, sync::mpsc::Receiver, task::Waker};
use std::{fs, path::PathBuf, sync::mpsc::Receiver};
use anyhow::{anyhow, bail, Context, Result};
use const_format::formatcp;
use crate::{
build::{run_make, BuildConfig, BuildStatus},
jobs::{start_job, update_status, Job, JobContext, JobResult, JobState},
app::AppConfig,
jobs::{
objdiff::{run_make, BuildConfig, BuildStatus},
start_job, update_status, Job, JobContext, JobResult, JobState,
},
};
#[derive(Debug, Clone)]
@@ -22,6 +26,38 @@ pub struct CreateScratchConfig {
pub preset_id: Option<u32>,
}
impl CreateScratchConfig {
pub(crate) fn from_config(config: &AppConfig, function_name: String) -> Result<Self> {
let Some(selected_obj) = &config.selected_obj else {
bail!("No object selected");
};
let Some(target_path) = &selected_obj.target_path else {
bail!("No target path for {}", selected_obj.name);
};
let Some(scratch_config) = &selected_obj.scratch else {
bail!("No scratch configuration for {}", selected_obj.name);
};
Ok(Self {
build_config: BuildConfig::from_config(config),
context_path: scratch_config.ctx_path.clone(),
build_context: scratch_config.build_ctx.unwrap_or(false),
compiler: scratch_config.compiler.clone().unwrap_or_default(),
platform: scratch_config.platform.clone().unwrap_or_default(),
compiler_flags: scratch_config.c_flags.clone().unwrap_or_default(),
function_name,
target_obj: target_path.to_path_buf(),
preset_id: scratch_config.preset_id,
})
}
pub fn is_available(config: &AppConfig) -> bool {
let Some(selected_obj) = &config.selected_obj else {
return false;
};
selected_obj.target_path.is_some() && selected_obj.scratch.is_some()
}
}
#[derive(Default, Debug, Clone)]
pub struct CreateScratchResult {
pub scratch_url: String,
@@ -63,7 +99,7 @@ fn run_create_scratch(
update_status(status, "Creating scratch".to_string(), 1, 2, &cancel)?;
let diff_flags = [format!("--disassemble={}", config.function_name)];
let diff_flags = serde_json::to_string(&diff_flags)?;
let diff_flags = serde_json::to_string(&diff_flags).unwrap();
let obj_path = project_dir.join(&config.target_obj);
let file = reqwest::blocking::multipart::Part::file(&obj_path)
.with_context(|| format!("Failed to open {}", obj_path.display()))?;
@@ -81,7 +117,7 @@ fn run_create_scratch(
form = form.part("target_obj", file);
let client = reqwest::blocking::Client::new();
let response = client
.post(format!("{API_HOST}/api/scratch"))
.post(formatcp!("{API_HOST}/api/scratch"))
.multipart(form)
.send()
.map_err(|e| anyhow!("Failed to send request: {}", e))?;
@@ -95,8 +131,8 @@ fn run_create_scratch(
Ok(Box::from(CreateScratchResult { scratch_url }))
}
pub fn start_create_scratch(waker: Waker, config: CreateScratchConfig) -> JobState {
start_job(waker, "Create scratch", Job::CreateScratch, move |context, cancel| {
pub fn start_create_scratch(ctx: &egui::Context, config: CreateScratchConfig) -> JobState {
start_job(ctx, "Create scratch", Job::CreateScratch, move |context, cancel| {
run_create_scratch(&context, cancel, config)
.map(|result| JobResult::CreateScratch(Some(result)))
})

View File

@@ -4,7 +4,6 @@ use std::{
mpsc::{Receiver, Sender, TryRecvError},
Arc, RwLock,
},
task::Waker,
thread::JoinHandle,
};
@@ -54,6 +53,7 @@ impl JobQueue {
}
/// Returns whether any job is running.
#[expect(dead_code)]
pub fn any_running(&self) -> bool {
self.jobs.iter().any(|job| {
if let Some(handle) = &job.handle {
@@ -96,53 +96,12 @@ impl JobQueue {
/// Removes a job from the queue given its ID.
pub fn remove(&mut self, id: usize) { self.jobs.retain(|job| job.id != id); }
/// Collects the results of all finished jobs and handles any errors.
pub fn collect_results(&mut self) {
let mut results = vec![];
for (job, result) in self.iter_finished() {
match result {
Ok(result) => {
match result {
JobResult::None => {
// Job context contains the error
}
_ => results.push(result),
}
}
Err(err) => {
let err = if let Some(msg) = err.downcast_ref::<&'static str>() {
anyhow::Error::msg(*msg)
} else if let Some(msg) = err.downcast_ref::<String>() {
anyhow::Error::msg(msg.clone())
} else {
anyhow::Error::msg("Thread panicked")
};
let result = job.context.status.write();
if let Ok(mut guard) = result {
guard.error = Some(err);
} else {
drop(result);
job.context.status = Arc::new(RwLock::new(JobStatus {
title: "Error".to_string(),
progress_percent: 0.0,
progress_items: None,
status: String::new(),
error: Some(err),
}));
}
}
}
}
self.results.append(&mut results);
self.clear_finished();
}
}
#[derive(Clone)]
pub struct JobContext {
pub status: Arc<RwLock<JobStatus>>,
pub waker: Waker,
pub egui: egui::Context,
}
pub struct JobState {
@@ -178,7 +137,7 @@ fn should_cancel(rx: &Receiver<()>) -> bool {
}
fn start_job(
waker: Waker,
ctx: &egui::Context,
title: &str,
kind: Job,
run: impl FnOnce(JobContext, Receiver<()>) -> Result<JobResult> + Send + 'static,
@@ -190,8 +149,8 @@ fn start_job(
status: String::new(),
error: None,
}));
let context = JobContext { status: status.clone(), waker: waker.clone() };
let context_inner = JobContext { status: status.clone(), waker };
let context = JobContext { status: status.clone(), egui: ctx.clone() };
let context_inner = JobContext { status: status.clone(), egui: ctx.clone() };
let (tx, rx) = std::sync::mpsc::channel();
let handle = std::thread::spawn(move || match run(context_inner, rx) {
Ok(state) => state,
@@ -203,7 +162,7 @@ fn start_job(
}
});
let id = JOB_ID.fetch_add(1, Ordering::Relaxed);
// log::info!("Started job {}", id); TODO
log::info!("Started job {}", id);
JobState { id, kind, handle: Some(handle), context, cancel: tx }
}
@@ -225,6 +184,6 @@ fn update_status(
w.status = str;
}
drop(w);
context.waker.wake_by_ref();
context.egui.request_repaint();
Ok(())
}

View File

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

View File

@@ -3,19 +3,14 @@ use std::{
fs::File,
path::PathBuf,
sync::mpsc::Receiver,
task::Waker,
};
use anyhow::{Context, Result};
pub use self_update; // Re-export self_update crate
use self_update::update::ReleaseUpdate;
use crate::jobs::{start_job, update_status, Job, JobContext, JobResult, JobState};
pub struct UpdateConfig {
pub build_updater: fn() -> Result<Box<dyn ReleaseUpdate>>,
pub bin_name: String,
}
use crate::{
jobs::{start_job, update_status, Job, JobContext, JobResult, JobState},
update::build_updater,
};
pub struct UpdateResult {
pub exe_path: PathBuf,
@@ -24,15 +19,16 @@ pub struct UpdateResult {
fn run_update(
status: &JobContext,
cancel: Receiver<()>,
config: UpdateConfig,
bin_name: String,
) -> Result<Box<UpdateResult>> {
update_status(status, "Fetching latest release".to_string(), 0, 3, &cancel)?;
let updater = (config.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 asset =
latest_release.assets.iter().find(|a| a.name == config.bin_name).ok_or_else(|| {
anyhow::Error::msg(format!("No release asset for {}", config.bin_name))
})?;
let asset = latest_release
.assets
.iter()
.find(|a| a.name == bin_name)
.ok_or_else(|| anyhow::Error::msg(format!("No release asset for {bin_name}")))?;
update_status(status, "Downloading release".to_string(), 1, 3, &cancel)?;
let tmp_dir = tempfile::Builder::new().prefix("update").tempdir_in(current_dir()?)?;
@@ -51,7 +47,9 @@ fn run_update(
#[cfg(unix)]
{
use std::{fs, os::unix::fs::PermissionsExt};
fs::set_permissions(&target_file, fs::Permissions::from_mode(0o755))?;
let mut perms = fs::metadata(&target_file)?.permissions();
perms.set_mode(0o755);
fs::set_permissions(&target_file, perms)?;
}
tmp_dir.close()?;
@@ -59,8 +57,8 @@ fn run_update(
Ok(Box::from(UpdateResult { exe_path: target_file }))
}
pub fn start_update(waker: Waker, config: UpdateConfig) -> JobState {
start_job(waker, "Update app", Job::Update, move |context, cancel| {
run_update(&context, cancel, config).map(JobResult::Update)
pub fn start_update(ctx: &egui::Context, bin_name: String) -> JobState {
start_job(ctx, "Update app", Job::Update, move |context, cancel| {
run_update(&context, cancel, bin_name).map(JobResult::Update)
})
}

View File

@@ -4,7 +4,6 @@ mod app;
mod app_config;
mod config;
mod fonts;
mod hotkeys;
mod jobs;
mod update;
mod views;
@@ -88,29 +87,14 @@ fn main() -> ExitCode {
}
#[cfg(feature = "wgpu")]
{
use eframe::egui_wgpu::{wgpu::Backends, WgpuSetup};
use eframe::egui_wgpu::wgpu::Backends;
if graphics_config.desired_backend.is_supported() {
native_options.wgpu_options.wgpu_setup = match native_options.wgpu_options.wgpu_setup {
WgpuSetup::CreateNew {
supported_backends: backends,
power_preference,
device_descriptor,
} => {
let backend = match graphics_config.desired_backend {
GraphicsBackend::Auto => backends,
GraphicsBackend::Dx12 => Backends::DX12,
GraphicsBackend::Metal => Backends::METAL,
GraphicsBackend::Vulkan => Backends::VULKAN,
GraphicsBackend::OpenGL => Backends::GL,
};
WgpuSetup::CreateNew {
supported_backends: backend,
power_preference,
device_descriptor,
}
}
// WgpuConfiguration::Default is CreateNew until we call run_eframe()
_ => unreachable!(),
native_options.wgpu_options.supported_backends = match graphics_config.desired_backend {
GraphicsBackend::Auto => native_options.wgpu_options.supported_backends,
GraphicsBackend::Dx12 => Backends::DX12,
GraphicsBackend::Metal => Backends::METAL,
GraphicsBackend::Vulkan => Backends::VULKAN,
GraphicsBackend::OpenGL => Backends::GL,
};
}
}
@@ -127,8 +111,6 @@ fn main() -> ExitCode {
}
#[cfg(feature = "wgpu")]
if let Some(e) = eframe_error {
use eframe::egui_wgpu::WgpuConfiguration;
// Attempt to relaunch using wgpu auto backend if the desired backend failed
#[allow(unused_mut)]
let mut should_relaunch = graphics_config.desired_backend != GraphicsBackend::Auto;
@@ -140,7 +122,7 @@ fn main() -> ExitCode {
if should_relaunch {
log::warn!("Failed to launch application: {e:?}");
log::warn!("Attempting to relaunch using auto-detected backend");
native_options.wgpu_options.wgpu_setup = WgpuConfiguration::default().wgpu_setup;
native_options.wgpu_options.supported_backends = Default::default();
if let Err(e) = run_eframe(
native_options.clone(),
utc_offset,

View File

@@ -1,7 +1,5 @@
use anyhow::Result;
use cfg_if::cfg_if;
use const_format::formatcp;
use objdiff_core::jobs::update::self_update;
use self_update::{cargo_crate_version, update::ReleaseUpdate};
pub const OS: &str = std::env::consts::OS;
@@ -28,8 +26,8 @@ pub const BIN_NAME_OLD: &str = formatcp!("objdiff-{}-{}{}", OS, ARCH, std::env::
pub const RELEASE_URL: &str =
formatcp!("https://github.com/{}/{}/releases/latest", GITHUB_USER, GITHUB_REPO);
pub fn build_updater() -> Result<Box<dyn ReleaseUpdate>> {
Ok(self_update::backends::github::Update::configure()
pub fn build_updater() -> self_update::errors::Result<Box<dyn ReleaseUpdate>> {
self_update::backends::github::Update::configure()
.repo_owner(GITHUB_USER)
.repo_name(GITHUB_REPO)
// bin_name is required, but unused?
@@ -37,5 +35,5 @@ pub fn build_updater() -> Result<Box<dyn ReleaseUpdate>> {
.no_confirm(true)
.show_output(false)
.current_version(cargo_crate_version!())
.build()?)
.build()
}

View File

@@ -14,18 +14,18 @@ use egui::{
use globset::Glob;
use objdiff_core::{
config::{ProjectObject, DEFAULT_WATCH_PATTERNS},
diff::{
ConfigEnum, ConfigEnumVariantInfo, ConfigPropertyId, ConfigPropertyKind,
ConfigPropertyValue, CONFIG_GROUPS,
},
jobs::{check_update::CheckUpdateResult, Job, JobQueue, JobResult},
diff::{ArmArchVersion, ArmR9Usage, MipsAbi, MipsInstrCategory, X86Formatter},
};
use strum::{EnumMessage, VariantArray};
use crate::{
app::{AppConfig, AppState, AppStateRef, ObjectConfig},
config::ProjectObjectNode,
hotkeys,
jobs::{start_check_update, start_update},
jobs::{
check_update::{start_check_update, CheckUpdateResult},
update::start_update,
Job, JobQueue, JobResult,
},
update::RELEASE_URL,
views::{
appearance::Appearance,
@@ -118,11 +118,11 @@ impl ConfigViewState {
if self.queue_check_update {
self.queue_check_update = false;
start_check_update(ctx, jobs);
jobs.push_once(Job::CheckUpdate, || start_check_update(ctx));
}
if let Some(bin_name) = self.queue_update.take() {
start_update(ctx, jobs, bin_name);
jobs.push_once(Job::Update, || start_update(ctx, bin_name));
}
}
}
@@ -254,11 +254,7 @@ pub fn config_ui(
}
} else {
let had_search = !config_state.object_search.is_empty();
let response =
egui::TextEdit::singleline(&mut config_state.object_search).hint_text("Filter").ui(ui);
if hotkeys::consume_object_filter_shortcut(ui.ctx()) {
response.request_focus();
}
egui::TextEdit::singleline(&mut config_state.object_search).hint_text("Filter").ui(ui);
let mut root_open = None;
let mut node_open = NodeOpen::Default;
@@ -876,102 +872,121 @@ pub fn arch_config_window(
});
}
fn config_property_ui(
ui: &mut egui::Ui,
state: &mut AppState,
property_id: ConfigPropertyId,
) -> bool {
let mut changed = false;
let current_value = state.config.diff_obj_config.get_property_value(property_id);
match (property_id.kind(), current_value) {
(ConfigPropertyKind::Boolean, ConfigPropertyValue::Boolean(mut checked)) => {
let mut response = ui.checkbox(&mut checked, property_id.name());
if let Some(description) = property_id.description() {
response = response.on_hover_text(description);
}
if response.changed() {
state
.config
.diff_obj_config
.set_property_value(property_id, ConfigPropertyValue::Boolean(checked))
.expect("Failed to set property value");
changed = true;
}
}
(ConfigPropertyKind::Choice(variants), ConfigPropertyValue::Choice(selected)) => {
fn variant_name(variant: &ConfigEnumVariantInfo) -> String {
if variant.is_default {
format!("{} (default)", variant.name)
} else {
variant.name.to_string()
fn arch_config_ui(ui: &mut egui::Ui, state: &mut AppState, _appearance: &Appearance) {
ui.heading("x86");
egui::ComboBox::new("x86_formatter", "Format")
.selected_text(state.config.diff_obj_config.x86_formatter.get_message().unwrap())
.show_ui(ui, |ui| {
for &formatter in X86Formatter::VARIANTS {
if ui
.selectable_label(
state.config.diff_obj_config.x86_formatter == formatter,
formatter.get_message().unwrap(),
)
.clicked()
{
state.config.diff_obj_config.x86_formatter = formatter;
state.queue_reload = true;
}
}
let selected_variant = variants
.iter()
.find(|v| v.value == selected)
.or_else(|| variants.iter().find(|v| v.is_default))
.expect("Invalid choice variant");
let response = egui::ComboBox::new(property_id.name(), property_id.name())
.selected_text(variant_name(selected_variant))
.show_ui(ui, |ui| {
for variant in variants {
let mut response =
ui.selectable_label(selected == variant.value, variant_name(variant));
if let Some(description) = variant.description {
response = response.on_hover_text(description);
}
if response.clicked() {
state
.config
.diff_obj_config
.set_property_value(
property_id,
ConfigPropertyValue::Choice(variant.value),
)
.expect("Failed to set property value");
changed = true;
}
}
})
.response;
if let Some(description) = property_id.description() {
response.on_hover_text(description);
});
ui.separator();
ui.heading("MIPS");
egui::ComboBox::new("mips_abi", "ABI")
.selected_text(state.config.diff_obj_config.mips_abi.get_message().unwrap())
.show_ui(ui, |ui| {
for &abi in MipsAbi::VARIANTS {
if ui
.selectable_label(
state.config.diff_obj_config.mips_abi == abi,
abi.get_message().unwrap(),
)
.clicked()
{
state.config.diff_obj_config.mips_abi = abi;
state.queue_reload = true;
}
}
}
_ => panic!("Incompatible property kind and value"),
});
egui::ComboBox::new("mips_instr_category", "Instruction Category")
.selected_text(state.config.diff_obj_config.mips_instr_category.get_message().unwrap())
.show_ui(ui, |ui| {
for &category in MipsInstrCategory::VARIANTS {
if ui
.selectable_label(
state.config.diff_obj_config.mips_instr_category == category,
category.get_message().unwrap(),
)
.clicked()
{
state.config.diff_obj_config.mips_instr_category = category;
state.queue_reload = true;
}
}
});
ui.separator();
ui.heading("ARM");
egui::ComboBox::new("arm_arch_version", "Architecture Version")
.selected_text(state.config.diff_obj_config.arm_arch_version.get_message().unwrap())
.show_ui(ui, |ui| {
for &version in ArmArchVersion::VARIANTS {
if ui
.selectable_label(
state.config.diff_obj_config.arm_arch_version == version,
version.get_message().unwrap(),
)
.clicked()
{
state.config.diff_obj_config.arm_arch_version = version;
state.queue_reload = true;
}
}
});
let response = ui
.checkbox(&mut state.config.diff_obj_config.arm_unified_syntax, "Unified syntax")
.on_hover_text("Disassemble as unified assembly language (UAL).");
if response.changed() {
state.queue_reload = true;
}
changed
}
fn arch_config_ui(ui: &mut egui::Ui, state: &mut AppState, _appearance: &Appearance) {
let mut first = true;
let mut changed = false;
for group in CONFIG_GROUPS {
if group.id == "general" {
continue;
}
if first {
first = false;
} else {
ui.separator();
}
ui.heading(group.name);
for property_id in group.properties.iter().cloned() {
changed |= config_property_ui(ui, state, property_id);
}
let response = ui
.checkbox(&mut state.config.diff_obj_config.arm_av_registers, "Use A/V registers")
.on_hover_text("Display R0-R3 as A1-A4 and R4-R11 as V1-V8");
if response.changed() {
state.queue_reload = true;
}
if changed {
state.queue_reload = true;
}
}
pub fn general_config_ui(ui: &mut egui::Ui, state: &mut AppState) {
let mut changed = false;
let group = CONFIG_GROUPS.iter().find(|group| group.id == "general").unwrap();
for property_id in group.properties.iter().cloned() {
changed |= config_property_ui(ui, state, property_id);
}
if changed {
egui::ComboBox::new("arm_r9_usage", "Display R9 as")
.selected_text(state.config.diff_obj_config.arm_r9_usage.get_message().unwrap())
.show_ui(ui, |ui| {
for &usage in ArmR9Usage::VARIANTS {
if ui
.selectable_label(
state.config.diff_obj_config.arm_r9_usage == usage,
usage.get_message().unwrap(),
)
.on_hover_text(usage.get_detailed_message().unwrap())
.clicked()
{
state.config.diff_obj_config.arm_r9_usage = usage;
state.queue_reload = true;
}
}
});
let response = ui
.checkbox(&mut state.config.diff_obj_config.arm_sl_usage, "Display R10 as SL")
.on_hover_text("Used for explicit stack limits.");
if response.changed() {
state.queue_reload = true;
}
let response = ui
.checkbox(&mut state.config.diff_obj_config.arm_fp_usage, "Display R11 as FP")
.on_hover_text("Used for frame pointers.");
if response.changed() {
state.queue_reload = true;
}
let response = ui
.checkbox(&mut state.config.diff_obj_config.arm_ip_usage, "Display R12 as IP")
.on_hover_text("Used for interworking and long branches.");
if response.changed() {
state.queue_reload = true;
}
}

View File

@@ -1,24 +1,17 @@
use std::{
cmp::{min, Ordering},
default::Default,
mem::take,
};
use std::{cmp::min, default::Default, mem::take};
use egui::{text::LayoutJob, Id, Label, RichText, Sense, Widget};
use objdiff_core::{
diff::{ObjDataDiff, ObjDataDiffKind, ObjDataRelocDiff, ObjDiff},
diff::{ObjDataDiff, ObjDataDiffKind, ObjDiff},
obj::ObjInfo,
};
use time::format_description;
use crate::{
hotkeys,
views::{
appearance::Appearance,
column_layout::{render_header, render_table},
symbol_diff::{DiffViewAction, DiffViewNavigation, DiffViewState},
write_text,
},
use crate::views::{
appearance::Appearance,
column_layout::{render_header, render_table},
symbol_diff::{DiffViewAction, DiffViewNavigation, DiffViewState},
write_text,
};
const BYTES_PER_ROW: usize = 16;
@@ -27,111 +20,8 @@ fn find_section(obj: &ObjInfo, section_name: &str) -> Option<usize> {
obj.sections.iter().position(|section| section.name == section_name)
}
fn data_row_hover_ui(
ui: &mut egui::Ui,
obj: &ObjInfo,
diffs: &[(ObjDataDiff, Vec<ObjDataRelocDiff>)],
appearance: &Appearance,
) {
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
let reloc_diffs = diffs.iter().flat_map(|(_, reloc_diffs)| reloc_diffs);
let mut prev_reloc = None;
for reloc_diff in reloc_diffs {
let reloc = &reloc_diff.reloc;
if prev_reloc == Some(reloc) {
// Avoid showing consecutive duplicate relocations.
// We do this because a single relocation can span across multiple diffs if the
// bytes in the relocation changed (e.g. first byte is added, second is unchanged).
continue;
}
prev_reloc = Some(reloc);
let color = get_color_for_diff_kind(reloc_diff.kind, appearance);
// TODO: Most of this code is copy-pasted from ins_hover_ui.
// Try to separate this out into a shared function.
ui.label(format!("Relocation type: {}", obj.arch.display_reloc(reloc.flags)));
ui.label(format!("Relocation address: {:x}", reloc.address));
let addend_str = match reloc.addend.cmp(&0i64) {
Ordering::Greater => format!("+{:x}", reloc.addend),
Ordering::Less => format!("-{:x}", -reloc.addend),
_ => "".to_string(),
};
ui.colored_label(color, format!("Name: {}{}", reloc.target.name, addend_str));
if let Some(orig_section_index) = reloc.target.orig_section_index {
if let Some(section) =
obj.sections.iter().find(|s| s.orig_index == orig_section_index)
{
ui.colored_label(color, format!("Section: {}", section.name));
}
ui.colored_label(
color,
format!("Address: {:x}{}", reloc.target.address, addend_str),
);
ui.colored_label(color, format!("Size: {:x}", reloc.target.size));
if reloc.addend >= 0 && reloc.target.bytes.len() > reloc.addend as usize {}
} else {
ui.colored_label(color, "Extern".to_string());
}
}
});
}
fn data_row_context_menu(ui: &mut egui::Ui, diffs: &[(ObjDataDiff, Vec<ObjDataRelocDiff>)]) {
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
let reloc_diffs = diffs.iter().flat_map(|(_, reloc_diffs)| reloc_diffs);
let mut prev_reloc = None;
for reloc_diff in reloc_diffs {
let reloc = &reloc_diff.reloc;
if prev_reloc == Some(reloc) {
// Avoid showing consecutive duplicate relocations.
// We do this because a single relocation can span across multiple diffs if the
// bytes in the relocation changed (e.g. first byte is added, second is unchanged).
continue;
}
prev_reloc = Some(reloc);
// TODO: This code is copy-pasted from ins_context_menu.
// Try to separate this out into a shared function.
if let Some(name) = &reloc.target.demangled_name {
if ui.button(format!("Copy \"{name}\"")).clicked() {
ui.output_mut(|output| output.copied_text.clone_from(name));
ui.close_menu();
}
}
if ui.button(format!("Copy \"{}\"", reloc.target.name)).clicked() {
ui.output_mut(|output| output.copied_text.clone_from(&reloc.target.name));
ui.close_menu();
}
}
});
}
fn get_color_for_diff_kind(diff_kind: ObjDataDiffKind, appearance: &Appearance) -> egui::Color32 {
match diff_kind {
ObjDataDiffKind::None => appearance.text_color,
ObjDataDiffKind::Replace => appearance.replace_color,
ObjDataDiffKind::Delete => appearance.delete_color,
ObjDataDiffKind::Insert => appearance.insert_color,
}
}
fn data_row_ui(
ui: &mut egui::Ui,
obj: Option<&ObjInfo>,
address: usize,
diffs: &[(ObjDataDiff, Vec<ObjDataRelocDiff>)],
appearance: &Appearance,
) {
if diffs.iter().any(|(dd, rds)| {
dd.kind != ObjDataDiffKind::None || rds.iter().any(|rd| rd.kind != ObjDataDiffKind::None)
}) {
fn data_row_ui(ui: &mut egui::Ui, address: usize, diffs: &[ObjDataDiff], appearance: &Appearance) {
if diffs.iter().any(|d| d.kind != ObjDataDiffKind::None) {
ui.painter().rect_filled(ui.available_rect_before_wrap(), 0.0, ui.visuals().faint_bg_color);
}
let mut job = LayoutJob::default();
@@ -141,34 +31,29 @@ fn data_row_ui(
&mut job,
appearance.code_font.clone(),
);
// The offset shown on the side of the GUI, shifted by insertions/deletions.
let mut cur_addr = 0usize;
// The offset into the actual bytes of the section on this side, ignoring differences.
let mut cur_addr_actual = address;
for (diff, reloc_diffs) in diffs {
let base_color = get_color_for_diff_kind(diff.kind, appearance);
for diff in diffs {
let base_color = match diff.kind {
ObjDataDiffKind::None => appearance.text_color,
ObjDataDiffKind::Replace => appearance.replace_color,
ObjDataDiffKind::Delete => appearance.delete_color,
ObjDataDiffKind::Insert => appearance.insert_color,
};
if diff.data.is_empty() {
let mut str = " ".repeat(diff.len);
str.push_str(" ".repeat(diff.len / 8).as_str());
write_text(str.as_str(), base_color, &mut job, appearance.code_font.clone());
cur_addr += diff.len;
} else {
let mut text = String::new();
for byte in &diff.data {
let mut byte_color = base_color;
if let Some(reloc_diff) = reloc_diffs.iter().find(|reloc_diff| {
reloc_diff.kind != ObjDataDiffKind::None
&& reloc_diff.range.contains(&cur_addr_actual)
}) {
byte_color = get_color_for_diff_kind(reloc_diff.kind, appearance);
}
let byte_text = format!("{byte:02x} ");
write_text(byte_text.as_str(), byte_color, &mut job, appearance.code_font.clone());
text.push_str(format!("{byte:02x} ").as_str());
cur_addr += 1;
cur_addr_actual += 1;
if cur_addr % 8 == 0 {
write_text(" ", base_color, &mut job, appearance.code_font.clone());
text.push(' ');
}
}
write_text(text.as_str(), base_color, &mut job, appearance.code_font.clone());
}
}
if cur_addr < BYTES_PER_ROW {
@@ -179,8 +64,13 @@ fn data_row_ui(
write_text(str.as_str(), appearance.text_color, &mut job, appearance.code_font.clone());
}
write_text(" ", appearance.text_color, &mut job, appearance.code_font.clone());
for (diff, _) in diffs {
let base_color = get_color_for_diff_kind(diff.kind, appearance);
for diff in diffs {
let base_color = match diff.kind {
ObjDataDiffKind::None => appearance.text_color,
ObjDataDiffKind::Replace => appearance.replace_color,
ObjDataDiffKind::Delete => appearance.delete_color,
ObjDataDiffKind::Insert => appearance.insert_color,
};
if diff.data.is_empty() {
write_text(
" ".repeat(diff.len).as_str(),
@@ -201,33 +91,22 @@ fn data_row_ui(
write_text(text.as_str(), base_color, &mut job, appearance.code_font.clone());
}
}
let response = Label::new(job).sense(Sense::click()).ui(ui);
if let Some(obj) = obj {
response
.on_hover_ui_at_pointer(|ui| data_row_hover_ui(ui, obj, diffs, appearance))
.context_menu(|ui| data_row_context_menu(ui, diffs));
}
Label::new(job).sense(Sense::click()).ui(ui);
// .on_hover_ui_at_pointer(|ui| ins_hover_ui(ui, ins))
// .context_menu(|ui| ins_context_menu(ui, ins));
}
fn split_diffs(
diffs: &[ObjDataDiff],
reloc_diffs: &[ObjDataRelocDiff],
) -> Vec<Vec<(ObjDataDiff, Vec<ObjDataRelocDiff>)>> {
let mut split_diffs = Vec::<Vec<(ObjDataDiff, Vec<ObjDataRelocDiff>)>>::new();
let mut row_diffs = Vec::<(ObjDataDiff, Vec<ObjDataRelocDiff>)>::new();
// The offset shown on the side of the GUI, shifted by insertions/deletions.
fn split_diffs(diffs: &[ObjDataDiff]) -> Vec<Vec<ObjDataDiff>> {
let mut split_diffs = Vec::<Vec<ObjDataDiff>>::new();
let mut row_diffs = Vec::<ObjDataDiff>::new();
let mut cur_addr = 0usize;
// The offset into the actual bytes of the section on this side, ignoring differences.
let mut cur_addr_actual = 0usize;
for diff in diffs {
let mut cur_len = 0usize;
while cur_len < diff.len {
let remaining_len = diff.len - cur_len;
let mut remaining_in_row = BYTES_PER_ROW - (cur_addr % BYTES_PER_ROW);
let len = min(remaining_len, remaining_in_row);
let data_diff = ObjDataDiff {
row_diffs.push(ObjDataDiff {
data: if diff.data.is_empty() {
Vec::new()
} else {
@@ -235,28 +114,9 @@ fn split_diffs(
},
kind: diff.kind,
len,
symbol: String::new(), // TODO
};
let row_reloc_diffs: Vec<ObjDataRelocDiff> = if diff.data.is_empty() {
Vec::new()
} else {
let diff_range = cur_addr_actual + cur_len..cur_addr_actual + cur_len + len;
reloc_diffs
.iter()
.filter_map(|reloc_diff| {
if reloc_diff.range.start < diff_range.end
&& diff_range.start < reloc_diff.range.end
{
Some(reloc_diff.clone())
} else {
None
}
})
.collect()
};
let row_diff = (data_diff, row_reloc_diffs);
row_diffs.push(row_diff);
// TODO
symbol: String::new(),
});
remaining_in_row -= len;
cur_len += len;
cur_addr += len;
@@ -264,7 +124,6 @@ fn split_diffs(
split_diffs.push(take(&mut row_diffs));
}
}
cur_addr_actual += diff.data.len();
}
if !row_diffs.is_empty() {
split_diffs.push(take(&mut row_diffs));
@@ -299,8 +158,6 @@ fn data_table_ui(
right_ctx: Option<SectionDiffContext<'_>>,
config: &Appearance,
) -> Option<()> {
let left_obj = left_ctx.map(|ctx| ctx.obj);
let right_obj = right_ctx.map(|ctx| ctx.obj);
let left_section = left_ctx
.and_then(|ctx| ctx.section_index.map(|i| (&ctx.obj.sections[i], &ctx.diff.sections[i])));
let right_section = right_ctx
@@ -316,12 +173,8 @@ fn data_table_ui(
}
let total_rows = (total_bytes - 1) / BYTES_PER_ROW + 1;
let left_diffs =
left_section.map(|(_, section)| split_diffs(&section.data_diff, &section.reloc_diff));
let right_diffs =
right_section.map(|(_, section)| split_diffs(&section.data_diff, &section.reloc_diff));
hotkeys::check_scroll_hotkeys(ui, true);
let left_diffs = left_section.map(|(_, section)| split_diffs(&section.data_diff));
let right_diffs = right_section.map(|(_, section)| split_diffs(&section.data_diff));
render_table(ui, available_width, 2, config.code_font.size, total_rows, |row, column| {
let i = row.index();
@@ -329,11 +182,11 @@ fn data_table_ui(
row.col(|ui| {
if column == 0 {
if let Some(left_diffs) = &left_diffs {
data_row_ui(ui, left_obj, address, &left_diffs[i], config);
data_row_ui(ui, address, &left_diffs[i], config);
}
} else if column == 1 {
if let Some(right_diffs) = &right_diffs {
data_row_ui(ui, right_obj, address, &right_diffs[i], config);
data_row_ui(ui, address, &right_diffs[i], config);
}
}
});
@@ -371,7 +224,7 @@ pub fn data_diff_ui(
render_header(ui, available_width, 2, |ui, column| {
if column == 0 {
// Left column
if ui.button("⏴ Back").clicked() || hotkeys::back_pressed(ui.ctx()) {
if ui.button("⏴ Back").clicked() {
ret = Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
}

View File

@@ -5,16 +5,13 @@ use objdiff_core::{
};
use time::format_description;
use crate::{
hotkeys,
views::{
appearance::Appearance,
column_layout::{render_header, render_strips},
function_diff::FunctionDiffContext,
symbol_diff::{
match_color_for_symbol, DiffViewAction, DiffViewNavigation, DiffViewState,
SymbolRefByName, View,
},
use crate::views::{
appearance::Appearance,
column_layout::{render_header, render_strips},
function_diff::FunctionDiffContext,
symbol_diff::{
match_color_for_symbol, DiffViewAction, DiffViewNavigation, DiffViewState, SymbolRefByName,
View,
},
};
@@ -139,7 +136,7 @@ pub fn extab_diff_ui(
if column == 0 {
// Left column
ui.horizontal(|ui| {
if ui.button("⏴ Back").clicked() || hotkeys::back_pressed(ui.ctx()) {
if ui.button("⏴ Back").clicked() {
ret = Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
}
ui.separator();
@@ -235,8 +232,6 @@ pub fn extab_diff_ui(
}
});
hotkeys::check_scroll_hotkeys(ui, true);
// Table
render_strips(ui, available_width, 2, |ui, column| {
if column == 0 {

View File

@@ -1,6 +1,6 @@
use std::{cmp::Ordering, default::Default};
use std::default::Default;
use egui::{text::LayoutJob, Id, Label, Layout, Response, RichText, Sense, Widget};
use egui::{text::LayoutJob, Id, Label, Response, RichText, Sense, Widget};
use egui_extras::TableRow;
use objdiff_core::{
diff::{
@@ -14,15 +14,12 @@ use objdiff_core::{
};
use time::format_description;
use crate::{
hotkeys,
views::{
appearance::Appearance,
column_layout::{render_header, render_strips, render_table},
symbol_diff::{
match_color_for_symbol, symbol_list_ui, DiffViewAction, DiffViewNavigation,
DiffViewState, SymbolDiffContext, SymbolFilter, SymbolRefByName, SymbolViewState, View,
},
use crate::views::{
appearance::Appearance,
column_layout::{render_header, render_strips, render_table},
symbol_diff::{
match_color_for_symbol, symbol_list_ui, DiffViewAction, DiffViewNavigation, DiffViewState,
SymbolDiffContext, SymbolFilter, SymbolRefByName, SymbolViewState, View,
},
};
@@ -123,15 +120,7 @@ fn ins_hover_ui(
if let Some(reloc) = &ins.reloc {
ui.label(format!("Relocation type: {}", obj.arch.display_reloc(reloc.flags)));
let addend_str = match reloc.addend.cmp(&0i64) {
Ordering::Greater => format!("+{:x}", reloc.addend),
Ordering::Less => format!("-{:x}", -reloc.addend),
_ => "".to_string(),
};
ui.colored_label(
appearance.highlight_color,
format!("Name: {}{}", reloc.target.name, addend_str),
);
ui.colored_label(appearance.highlight_color, format!("Name: {}", reloc.target.name));
if let Some(orig_section_index) = reloc.target.orig_section_index {
if let Some(section) =
obj.sections.iter().find(|s| s.orig_index == orig_section_index)
@@ -143,13 +132,17 @@ fn ins_hover_ui(
}
ui.colored_label(
appearance.highlight_color,
format!("Address: {:x}{}", reloc.target.address, addend_str),
format!("Address: {:x}", reloc.target.address),
);
ui.colored_label(
appearance.highlight_color,
format!("Size: {:x}", reloc.target.size),
);
if let Some(s) = obj.arch.display_ins_data(ins) {
if let Some(s) = obj
.arch
.guess_data_type(ins)
.and_then(|ty| obj.arch.display_data_type(ty, &reloc.target.bytes))
{
ui.colored_label(appearance.highlight_color, s);
}
} else {
@@ -157,8 +150,8 @@ fn ins_hover_ui(
}
}
if let Some(decoded) = rlwinmdec::decode(&ins.formatted) {
ui.colored_label(appearance.highlight_color, decoded.trim());
if let Some(demangled) = rlwinmdec::decode(&ins.formatted) {
ui.colored_label(appearance.highlight_color, demangled.trim());
}
});
}
@@ -410,7 +403,6 @@ fn asm_col_ui(
}
#[must_use]
#[expect(clippy::too_many_arguments)]
fn asm_table_ui(
ui: &mut egui::Ui,
available_width: f32,
@@ -419,7 +411,6 @@ fn asm_table_ui(
appearance: &Appearance,
ins_view_state: &FunctionViewState,
symbol_state: &SymbolViewState,
open_sections: (Option<bool>, Option<bool>),
) -> Option<DiffViewAction> {
let mut ret = None;
let left_len = left_ctx.and_then(|ctx| {
@@ -445,7 +436,6 @@ fn asm_table_ui(
};
if left_len.is_some() && right_len.is_some() {
// Joint view
hotkeys::check_scroll_hotkeys(ui, true);
render_table(
ui,
available_width,
@@ -481,7 +471,6 @@ fn asm_table_ui(
if column == 0 {
if let Some(ctx) = left_ctx {
if ctx.has_symbol() {
hotkeys::check_scroll_hotkeys(ui, false);
render_table(
ui,
available_width / 2.0,
@@ -510,7 +499,6 @@ fn asm_table_ui(
SymbolFilter::Mapping(right_symbol_ref),
appearance,
column,
open_sections.0,
) {
match action {
DiffViewAction::Navigate(DiffViewNavigation {
@@ -528,6 +516,9 @@ fn asm_table_ui(
SymbolRefByName::new(right_symbol, right_section),
));
}
DiffViewAction::SetSymbolHighlight(_, _) => {
// Ignore
}
_ => {
ret = Some(action);
}
@@ -540,7 +531,6 @@ fn asm_table_ui(
} else if column == 1 {
if let Some(ctx) = right_ctx {
if ctx.has_symbol() {
hotkeys::check_scroll_hotkeys(ui, false);
render_table(
ui,
available_width / 2.0,
@@ -569,7 +559,6 @@ fn asm_table_ui(
SymbolFilter::Mapping(left_symbol_ref),
appearance,
column,
open_sections.1,
) {
match action {
DiffViewAction::Navigate(DiffViewNavigation {
@@ -587,6 +576,9 @@ fn asm_table_ui(
right_symbol_ref,
));
}
DiffViewAction::SetSymbolHighlight(_, _) => {
// Ignore
}
_ => {
ret = Some(action);
}
@@ -683,12 +675,11 @@ pub fn function_diff_ui(
// Header
let available_width = ui.available_width();
let mut open_sections = (None, None);
render_header(ui, available_width, 2, |ui, column| {
if column == 0 {
// Left column
ui.horizontal(|ui| {
if ui.button("⏴ Back").clicked() || hotkeys::back_pressed(ui.ctx()) {
if ui.button("⏴ Back").clicked() {
ret = Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
}
ui.separator();
@@ -721,11 +712,10 @@ pub fn function_diff_ui(
.color(appearance.highlight_color),
);
if right_ctx.is_some_and(|m| m.has_symbol())
&& (ui
&& ui
.button("Change target")
.on_hover_text_at_pointer("Choose a different symbol to use as the target")
.clicked()
|| hotkeys::consume_change_target_shortcut(ui.ctx()))
{
if let Some(symbol_ref) = state.symbol_state.right_symbol.as_ref() {
ret = Some(DiffViewAction::SelectingLeft(symbol_ref.clone()));
@@ -737,24 +727,11 @@ pub fn function_diff_ui(
.font(appearance.code_font.clone())
.color(appearance.replace_color),
);
ui.horizontal(|ui| {
ui.label(
RichText::new("Choose target symbol")
.font(appearance.code_font.clone())
.color(appearance.highlight_color),
);
ui.with_layout(Layout::right_to_left(egui::Align::TOP), |ui| {
if ui.small_button("").on_hover_text_at_pointer("Expand all").clicked() {
open_sections.0 = Some(true);
}
if ui.small_button("").on_hover_text_at_pointer("Collapse all").clicked()
{
open_sections.0 = Some(false);
}
})
});
ui.label(
RichText::new("Choose target symbol")
.font(appearance.code_font.clone())
.color(appearance.highlight_color),
);
}
} else if column == 1 {
// Right column
@@ -812,7 +789,6 @@ pub fn function_diff_ui(
"Choose a different symbol to use as the base",
)
.clicked()
|| hotkeys::consume_change_base_shortcut(ui.ctx())
{
if let Some(symbol_ref) = state.symbol_state.left_symbol.as_ref() {
ret = Some(DiffViewAction::SelectingRight(symbol_ref.clone()));
@@ -826,24 +802,11 @@ pub fn function_diff_ui(
.font(appearance.code_font.clone())
.color(appearance.replace_color),
);
ui.horizontal(|ui| {
ui.label(
RichText::new("Choose base symbol")
.font(appearance.code_font.clone())
.color(appearance.highlight_color),
);
ui.with_layout(Layout::right_to_left(egui::Align::TOP), |ui| {
if ui.small_button("").on_hover_text_at_pointer("Expand all").clicked() {
open_sections.1 = Some(true);
}
if ui.small_button("").on_hover_text_at_pointer("Collapse all").clicked()
{
open_sections.1 = Some(false);
}
})
});
ui.label(
RichText::new("Choose base symbol")
.font(appearance.code_font.clone())
.color(appearance.highlight_color),
);
}
}
});
@@ -861,7 +824,6 @@ pub fn function_diff_ui(
appearance,
&state.function_state,
&state.symbol_state,
open_sections,
)
})
.inner

View File

@@ -1,9 +1,11 @@
use std::cmp::Ordering;
use egui::{ProgressBar, RichText, Widget};
use objdiff_core::jobs::{JobQueue, JobStatus};
use crate::views::appearance::Appearance;
use crate::{
jobs::{JobQueue, JobStatus},
views::appearance::Appearance,
};
pub fn jobs_ui(ui: &mut egui::Ui, jobs: &mut JobQueue, appearance: &Appearance) {
if ui.button("Clear").clicked() {

View File

@@ -16,13 +16,13 @@ pub fn rlwinm_decode_window(
egui::Window::new("Rlwinm Decoder").open(show).show(ctx, |ui| {
ui.text_edit_singleline(&mut state.text);
ui.add_space(10.0);
if let Some(decoded) = rlwinmdec::decode(&state.text) {
if let Some(demangled) = rlwinmdec::decode(&state.text) {
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(TextStyle::Monospace);
ui.colored_label(appearance.replace_color, decoded.trim());
ui.colored_label(appearance.replace_color, demangled.trim());
});
if ui.button("Copy").clicked() {
ui.output_mut(|output| output.copied_text = decoded);
ui.output_mut(|output| output.copied_text = demangled);
}
} else {
ui.scope(|ui| {

View File

@@ -1,14 +1,12 @@
use std::{collections::BTreeMap, mem::take, ops::Bound};
use std::{collections::BTreeMap, mem::take};
use egui::{
style::ScrollAnimation, text::LayoutJob, CollapsingHeader, Color32, Id, Layout, OpenUrl,
ScrollArea, SelectableLabel, TextEdit, Ui, Widget,
text::LayoutJob, CollapsingHeader, Color32, Id, OpenUrl, ScrollArea, SelectableLabel, TextEdit,
Ui, Widget,
};
use objdiff_core::{
arch::ObjArch,
build::BuildStatus,
diff::{display::HighlightKind, ObjDiff, ObjSymbolDiff},
jobs::{create_scratch::CreateScratchResult, objdiff::ObjDiffResult, Job, JobQueue, JobResult},
obj::{
ObjInfo, ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlags, SymbolRef, SECTION_COMMON,
},
@@ -17,8 +15,11 @@ use regex::{Regex, RegexBuilder};
use crate::{
app::AppStateRef,
hotkeys,
jobs::{is_create_scratch_available, start_create_scratch},
jobs::{
create_scratch::{start_create_scratch, CreateScratchConfig, CreateScratchResult},
objdiff::{BuildStatus, ObjDiffResult},
Job, JobQueue, JobResult,
},
views::{
appearance::Appearance,
column_layout::{render_header, render_strips},
@@ -55,8 +56,8 @@ pub enum DiffViewAction {
Build,
/// Navigate to a new diff view
Navigate(DiffViewNavigation),
/// Set the highlighted symbols in the symbols view, optionally scrolling them into view.
SetSymbolHighlight(Option<SymbolRef>, Option<SymbolRef>, bool),
/// Set the highlighted symbols in the symbols view
SetSymbolHighlight(Option<SymbolRef>, Option<SymbolRef>),
/// Set the symbols view search filter
SetSearch(String),
/// Submit the current function to decomp.me
@@ -134,7 +135,6 @@ pub struct DiffViewState {
#[derive(Default)]
pub struct SymbolViewState {
pub highlighted_symbol: (Option<SymbolRef>, Option<SymbolRef>),
pub autoscroll_to_highlighted_symbols: bool,
pub left_symbol: Option<SymbolRefByName>,
pub right_symbol: Option<SymbolRefByName>,
pub reverse_fn_order: bool,
@@ -180,7 +180,7 @@ impl DiffViewState {
} else {
self.source_path_available = false;
}
self.scratch_available = is_create_scratch_available(&state.config);
self.scratch_available = CreateScratchConfig::is_available(&state.config);
self.object_name =
state.config.selected_obj.as_ref().map(|o| o.name.clone()).unwrap_or_default();
}
@@ -197,9 +197,6 @@ impl DiffViewState {
ctx.output_mut(|o| o.open_url = Some(OpenUrl::new_tab(result.scratch_url)));
}
// Clear the autoscroll flag so that it doesn't scroll continuously.
self.symbol_state.autoscroll_to_highlighted_symbols = false;
let Some(action) = action else {
return;
};
@@ -214,6 +211,7 @@ impl DiffViewState {
// Ignore action if we're already navigating
return;
}
self.symbol_state.highlighted_symbol = (None, None);
let Ok(mut state) = state.write() else {
return;
};
@@ -249,9 +247,8 @@ impl DiffViewState {
self.post_build_nav = Some(nav);
}
}
DiffViewAction::SetSymbolHighlight(left, right, autoscroll) => {
DiffViewAction::SetSymbolHighlight(left, right) => {
self.symbol_state.highlighted_symbol = (left, right);
self.symbol_state.autoscroll_to_highlighted_symbols = autoscroll;
}
DiffViewAction::SetSearch(search) => {
self.search_regex = if search.is_empty() {
@@ -268,7 +265,14 @@ impl DiffViewState {
let Ok(state) = state.read() else {
return;
};
start_create_scratch(ctx, jobs, &state, function_name);
match CreateScratchConfig::from_config(&state.config, function_name) {
Ok(config) => {
jobs.push_once(Job::CreateScratch, || start_create_scratch(ctx, config));
}
Err(err) => {
log::error!("Failed to create scratch config: {err}");
}
}
}
DiffViewAction::OpenSourcePath => {
let Ok(state) = state.read() else {
@@ -530,15 +534,7 @@ fn symbol_ui(
ret = Some(DiffViewAction::Navigate(result));
}
});
if selected && state.autoscroll_to_highlighted_symbols {
// Automatically scroll the view to encompass the selected symbol in case the user selected
// an offscreen symbol by using a keyboard shortcut.
ui.scroll_to_rect_animation(response.rect, None, ScrollAnimation::none());
// This autoscroll state flag will be reset in DiffViewState::post_update at the end of
// every frame so that we don't continuously scroll the view back when the user is trying to
// manually scroll away.
}
if response.clicked() || (selected && hotkeys::enter_pressed(ui.ctx())) {
if response.clicked() {
if let Some(section) = section {
match section.kind {
ObjSectionKind::Code => {
@@ -565,18 +561,20 @@ fn symbol_ui(
}
}
} else if response.hovered() {
ret = Some(if column == 0 {
DiffViewAction::SetSymbolHighlight(
Some(symbol_diff.symbol_ref),
symbol_diff.target_symbol,
false,
)
ret = Some(if let Some(target_symbol) = symbol_diff.target_symbol {
if column == 0 {
DiffViewAction::SetSymbolHighlight(
Some(symbol_diff.symbol_ref),
Some(target_symbol),
)
} else {
DiffViewAction::SetSymbolHighlight(
Some(target_symbol),
Some(symbol_diff.symbol_ref),
)
}
} else {
DiffViewAction::SetSymbolHighlight(
symbol_diff.target_symbol,
Some(symbol_diff.symbol_ref),
false,
)
DiffViewAction::SetSymbolHighlight(None, None)
});
}
ret
@@ -605,7 +603,6 @@ pub enum SymbolFilter<'a> {
}
#[must_use]
#[expect(clippy::too_many_arguments)]
pub fn symbol_list_ui(
ui: &mut Ui,
ctx: SymbolDiffContext<'_>,
@@ -614,7 +611,6 @@ pub fn symbol_list_ui(
filter: SymbolFilter<'_>,
appearance: &Appearance,
column: usize,
open_sections: Option<bool>,
) -> Option<DiffViewAction> {
let mut ret = None;
ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| {
@@ -652,58 +648,6 @@ pub fn symbol_list_ui(
}
}
hotkeys::check_scroll_hotkeys(ui, false);
let mut new_key_value_to_highlight = None;
if let Some(sym_ref) =
if column == 0 { state.highlighted_symbol.0 } else { state.highlighted_symbol.1 }
{
let up = if hotkeys::consume_up_key(ui.ctx()) {
Some(true)
} else if hotkeys::consume_down_key(ui.ctx()) {
Some(false)
} else {
None
};
if let Some(mut up) = up {
if state.reverse_fn_order {
up = !up;
}
new_key_value_to_highlight = if up {
mapping.range(..sym_ref).next_back()
} else {
mapping.range((Bound::Excluded(sym_ref), Bound::Unbounded)).next()
};
};
} else {
// No symbol is highlighted in this column. Select the topmost symbol instead.
// Note that we intentionally do not consume the up/down key presses in this case, but
// we do when a symbol is highlighted. This is so that if only one column has a symbol
// highlighted, that one takes precedence over the one with nothing highlighted.
if hotkeys::up_pressed(ui.ctx()) || hotkeys::down_pressed(ui.ctx()) {
new_key_value_to_highlight = if state.reverse_fn_order {
mapping.last_key_value()
} else {
mapping.first_key_value()
};
}
}
if let Some((new_sym_ref, new_symbol_diff)) = new_key_value_to_highlight {
ret = Some(if column == 0 {
DiffViewAction::SetSymbolHighlight(
Some(*new_sym_ref),
new_symbol_diff.target_symbol,
true,
)
} else {
DiffViewAction::SetSymbolHighlight(
new_symbol_diff.target_symbol,
Some(*new_sym_ref),
true,
)
});
}
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
@@ -768,7 +712,6 @@ pub fn symbol_list_ui(
CollapsingHeader::new(header)
.id_salt(Id::new(section.name.clone()).with(section.orig_index))
.default_open(true)
.open(open_sections)
.show(ui, |ui| {
if section.kind == ObjSectionKind::Code && state.reverse_fn_order {
for (symbol, symbol_diff) in mapping
@@ -876,7 +819,6 @@ pub fn symbol_diff_ui(
// Header
let available_width = ui.available_width();
let mut open_sections = (None, None);
render_header(ui, available_width, 2, |ui, column| {
if column == 0 {
// Left column
@@ -895,25 +837,10 @@ pub fn symbol_diff_ui(
}
});
ui.horizontal(|ui| {
let mut search = state.search.clone();
let response = TextEdit::singleline(&mut search).hint_text("Filter symbols").ui(ui);
if hotkeys::consume_symbol_filter_shortcut(ui.ctx()) {
response.request_focus();
}
if response.changed() {
ret = Some(DiffViewAction::SetSearch(search));
}
ui.with_layout(Layout::right_to_left(egui::Align::TOP), |ui| {
if ui.small_button("").on_hover_text_at_pointer("Expand all").clicked() {
open_sections.0 = Some(true);
}
if ui.small_button("").on_hover_text_at_pointer("Collapse all").clicked() {
open_sections.0 = Some(false);
}
})
});
let mut search = state.search.clone();
if TextEdit::singleline(&mut search).hint_text("Filter symbols").ui(ui).changed() {
ret = Some(DiffViewAction::SetSearch(search));
}
} else if column == 1 {
// Right column
ui.horizontal(|ui| {
@@ -945,20 +872,9 @@ pub fn symbol_diff_ui(
}
});
ui.horizontal(|ui| {
if ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() {
ret = Some(DiffViewAction::Build);
}
ui.with_layout(Layout::right_to_left(egui::Align::TOP), |ui| {
if ui.small_button("").on_hover_text_at_pointer("Expand all").clicked() {
open_sections.1 = Some(true);
}
if ui.small_button("").on_hover_text_at_pointer("Collapse all").clicked() {
open_sections.1 = Some(false);
}
})
});
if ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() {
ret = Some(DiffViewAction::Build);
}
}
});
@@ -983,7 +899,6 @@ pub fn symbol_diff_ui(
filter,
appearance,
column,
open_sections.0,
) {
ret = Some(result);
}
@@ -1008,7 +923,6 @@ pub fn symbol_diff_ui(
filter,
appearance,
column,
open_sections.1,
) {
ret = Some(result);
}

View File

@@ -1,6 +1,6 @@
{
"name": "objdiff-wasm",
"version": "2.6.0",
"version": "2.0.0",
"description": "A local diffing tool for decompilation projects.",
"author": {
"name": "Luke Street",
@@ -21,7 +21,7 @@
"build": "tsup",
"build:all": "npm run build:wasm && npm run build:proto && npm run build",
"build:proto": "protoc --ts_out=gen --ts_opt add_pb_suffix,eslint_disable,ts_nocheck,use_proto_field_name --proto_path=../objdiff-core/protos ../objdiff-core/protos/*.proto",
"build:wasm": "cd ../objdiff-core && wasm-pack build --out-dir ../objdiff-wasm/pkg --target web -- --features arm,arm64,dwarf,config,ppc,x86,wasm"
"build:wasm": "cd ../objdiff-core && wasm-pack build --out-dir ../objdiff-wasm/pkg --target web -- --features arm,dwarf,ppc,x86,wasm"
},
"dependencies": {
"@protobuf-ts/runtime": "^2.9.4"

View File

@@ -111,12 +111,12 @@ async function defer<T>(message: AnyHandlerData, worker?: Worker): Promise<T> {
return promise;
}
export async function runDiff(left: Uint8Array | undefined, right: Uint8Array | undefined, diff_config?: DiffObjConfig): Promise<DiffResult> {
export async function runDiff(left: Uint8Array | undefined, right: Uint8Array | undefined, config?: DiffObjConfig): Promise<DiffResult> {
const data = await defer<Uint8Array>({
type: 'run_diff_proto',
left,
right,
diff_config
config
});
const parseStart = performance.now();
const result = DiffResult.fromBinary(data, {readUnknownField: false});
@@ -194,17 +194,12 @@ export function displayDiff(diff: InstructionDiff, baseAddr: bigint, cb: (text:
cb({type: 'spacing', count: 4});
}
cb({type: 'opcode', mnemonic: ins.mnemonic, opcode: ins.opcode});
let arg_diff_idx = 0; // non-PlainText argument index
for (let i = 0; i < ins.arguments.length; i++) {
if (i === 0) {
cb({type: 'spacing', count: 1});
}
const arg = ins.arguments[i].value;
let diff_index: number | undefined;
if (arg.oneofKind !== 'plain_text') {
diff_index = diff.arg_diff[arg_diff_idx]?.diff_index;
arg_diff_idx++;
}
const diff_index = diff.arg_diff[i]?.diff_index;
switch (arg.oneofKind) {
case "plain_text":
cb({type: 'basic', text: arg.plain_text, diff_index});

View File

@@ -38,15 +38,13 @@ async function initIfNeeded() {
// return exports.run_diff_json(left, right, cfg);
// }
async function run_diff_proto({left, right, diff_config, mapping_config}: {
async function run_diff_proto({left, right, config}: {
left: Uint8Array | undefined,
right: Uint8Array | undefined,
diff_config?: exports.DiffObjConfig,
mapping_config?: exports.MappingConfig,
config?: exports.DiffObjConfig,
}): Promise<Uint8Array> {
diff_config = diff_config || {};
mapping_config = mapping_config || {};
return exports.run_diff_proto(left, right, diff_config, mapping_config);
config = config || {};
return exports.run_diff_proto(left, right, config);
}
export type AnyHandlerData = HandlerData[keyof HandlerData];
@@ -75,19 +73,12 @@ self.onmessage = (event: MessageEvent<InMessage>) => {
const result = await handler(data as never);
const end = performance.now();
console.debug(`Worker message ${data.messageId} took ${end - start}ms`);
let transfer: Transferable[] = [];
if (result instanceof Uint8Array) {
console.log("Transferring!", result.byteLength);
transfer = [result.buffer];
} else {
console.log("Didn't transfer", typeof result);
}
self.postMessage({
type: 'result',
result: result,
error: null,
messageId,
} as OutMessage, {transfer});
} as OutMessage);
} else {
throw new Error(`No handler for ${data.type}`);
}