mirror of
https://github.com/encounter/objdiff.git
synced 2025-12-16 08:27:04 +00:00
Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f7efe5fdff | |||
| 0692deac59 | |||
| c3e3d175c5 | |||
| c45f4bbc99 | |||
| b0c5431ac5 | |||
|
|
9ab246367b | ||
|
|
dcafe51eda | ||
| c65e87c382 | |||
| 1756b9f6c5 | |||
| 303f2938a2 | |||
| 526e031251 | |||
|
|
10b2a9c129 | ||
|
|
abe68ef2f2 | ||
|
|
304df96411 | ||
| 7aa878b48e | |||
| a119d9a6dd | |||
|
|
ebf653816a | ||
| 424434edd6 | |||
| 7f14b684bf | |||
| c5da7f7dd5 | |||
| 2fd655850a | |||
| 79bd7317c1 | |||
| 21f8f2407c | |||
| d2b7a9ef25 | |||
| 2cf9cf24d6 | |||
|
|
5ef3416457 | ||
|
|
6ff8d002f7 | ||
| 9ca157d717 | |||
|
|
67b63311fc | ||
| 72ea1c8911 | |||
| d4a540857d | |||
| 676488433f |
5
.cargo/config.toml
Normal file
5
.cargo/config.toml
Normal file
@@ -0,0 +1,5 @@
|
||||
[target.x86_64-pc-windows-msvc]
|
||||
linker = "rust-lld"
|
||||
|
||||
[target.aarch64-pc-windows-msvc]
|
||||
linker = "rust-lld"
|
||||
12
.github/workflows/build.yaml
vendored
12
.github/workflows/build.yaml
vendored
@@ -30,6 +30,8 @@ jobs:
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: clippy
|
||||
- name: Cache Rust workspace
|
||||
uses: Swatinem/rust-cache@v2
|
||||
- name: Cargo check
|
||||
run: cargo check --all-features --all-targets
|
||||
- name: Cargo clippy
|
||||
@@ -85,6 +87,8 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- name: Cache Rust workspace
|
||||
uses: Swatinem/rust-cache@v2
|
||||
- name: Cargo test
|
||||
run: cargo test --release --all-features
|
||||
|
||||
@@ -151,6 +155,10 @@ jobs:
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
- name: Cache Rust workspace
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
key: ${{ matrix.target }}
|
||||
- name: Cargo build
|
||||
run: >
|
||||
cargo ${{ matrix.build }} --profile ${{ env.BUILD_PROFILE }} --target ${{ matrix.target }}
|
||||
@@ -202,6 +210,10 @@ jobs:
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
- name: Cache Rust workspace
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
key: ${{ matrix.target }}
|
||||
- name: Cargo build
|
||||
run: >
|
||||
cargo build --profile ${{ env.BUILD_PROFILE }} --target ${{ matrix.target }}
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -3,10 +3,6 @@ target/
|
||||
**/*.rs.bk
|
||||
generated/
|
||||
|
||||
# cargo-mobile
|
||||
.cargo/
|
||||
/gen
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
@@ -22,4 +18,4 @@ android.keystore
|
||||
*.frag
|
||||
*.vert
|
||||
*.metal
|
||||
.vscode/launch.json
|
||||
.vscode/
|
||||
|
||||
1618
Cargo.lock
generated
1618
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -13,7 +13,7 @@ strip = "debuginfo"
|
||||
codegen-units = 1
|
||||
|
||||
[workspace.package]
|
||||
version = "2.3.1"
|
||||
version = "2.6.0"
|
||||
authors = ["Luke Street <luke@street.dev>"]
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
@@ -20,6 +20,7 @@ Supports:
|
||||
- MIPS (N64, PS1, PS2, PSP)
|
||||
- x86 (COFF only at the moment)
|
||||
- ARM (GBA, DS, 3DS)
|
||||
- ARM64 (Switch, experimental)
|
||||
|
||||
See [Usage](#usage) for more information.
|
||||
|
||||
|
||||
@@ -70,10 +70,10 @@ feature-depth = 1
|
||||
# A list of advisory IDs to ignore. Note that ignored advisories will still
|
||||
# output a note when they are encountered.
|
||||
ignore = [
|
||||
"RUSTSEC-2024-0370",
|
||||
#{ 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.
|
||||
@@ -98,7 +98,7 @@ allow = [
|
||||
"BSL-1.0",
|
||||
"CC0-1.0",
|
||||
"MPL-2.0",
|
||||
"Unicode-DFS-2016",
|
||||
"Unicode-3.0",
|
||||
"Zlib",
|
||||
"0BSD",
|
||||
"OFL-1.1",
|
||||
@@ -240,7 +240,7 @@ allow-git = []
|
||||
|
||||
[sources.allow-org]
|
||||
# github.com organizations to allow git sources for
|
||||
github = ["encounter"]
|
||||
github = ["notify-rs"]
|
||||
# gitlab.com organizations to allow git sources for
|
||||
gitlab = []
|
||||
# bitbucket.org organizations to allow git sources for
|
||||
|
||||
@@ -14,13 +14,13 @@ publish = false
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
argp = "0.3"
|
||||
argp = "0.4"
|
||||
crossterm = "0.28"
|
||||
enable-ansi-support = "0.2"
|
||||
memmap2 = "0.9"
|
||||
objdiff-core = { path = "../objdiff-core", features = ["all"] }
|
||||
prost = "0.13"
|
||||
ratatui = "0.28"
|
||||
ratatui = "0.29"
|
||||
rayon = "1.10"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -169,22 +169,26 @@ fn report_object(
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
let config = diff::DiffObjConfig { relax_reloc_diffs: true, ..Default::default() };
|
||||
let diff_config = diff::DiffObjConfig { relax_reloc_diffs: true, ..Default::default() };
|
||||
let mapping_config = diff::MappingConfig::default();
|
||||
let target = object
|
||||
.target_path
|
||||
.as_ref()
|
||||
.map(|p| {
|
||||
obj::read::read(p, &config).with_context(|| format!("Failed to open {}", p.display()))
|
||||
obj::read::read(p, &diff_config)
|
||||
.with_context(|| format!("Failed to open {}", p.display()))
|
||||
})
|
||||
.transpose()?;
|
||||
let base = object
|
||||
.base_path
|
||||
.as_ref()
|
||||
.map(|p| {
|
||||
obj::read::read(p, &config).with_context(|| format!("Failed to open {}", p.display()))
|
||||
obj::read::read(p, &diff_config)
|
||||
.with_context(|| format!("Failed to open {}", p.display()))
|
||||
})
|
||||
.transpose()?;
|
||||
let result = diff::diff_objs(&config, target.as_ref(), base.as_ref(), None)?;
|
||||
let result =
|
||||
diff::diff_objs(&diff_config, &mapping_config, target.as_ref(), base.as_ref(), None)?;
|
||||
|
||||
let metadata = ReportUnitMetadata {
|
||||
complete: object.complete(),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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.
|
||||
|
||||
653
objdiff-cli/src/views/function_diff.rs
Normal file
653
objdiff-cli/src/views/function_diff.rs
Normal file
@@ -0,0 +1,653 @@
|
||||
use anyhow::{bail, Result};
|
||||
use crossterm::event::{Event, KeyCode, KeyEventKind, KeyModifiers, MouseButton, MouseEventKind};
|
||||
use objdiff_core::{
|
||||
diff::{
|
||||
display::{display_diff, DiffText, HighlightKind},
|
||||
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;
|
||||
}
|
||||
// Toggle relax relocation diffs
|
||||
KeyCode::Char('x') => {
|
||||
state.diff_obj_config.relax_reloc_diffs =
|
||||
!state.diff_obj_config.relax_reloc_diffs;
|
||||
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
|
||||
}
|
||||
25
objdiff-cli/src/views/mod.rs
Normal file
25
objdiff-cli/src/views/mod.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
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<()>;
|
||||
}
|
||||
@@ -16,16 +16,104 @@ documentation = "https://docs.rs/objdiff-core"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[features]
|
||||
all = ["config", "dwarf", "mips", "ppc", "x86", "arm", "bindings"]
|
||||
any-arch = ["bimap"] # Implicit, used to check if any arch is enabled
|
||||
config = ["bimap", "globset", "semver", "serde_json", "serde_yaml"]
|
||||
dwarf = ["gimli"]
|
||||
mips = ["any-arch", "rabbitizer"]
|
||||
ppc = ["any-arch", "cwdemangle", "cwextab", "ppc750cl"]
|
||||
x86 = ["any-arch", "cpp_demangle", "iced-x86", "msvc-demangler"]
|
||||
arm = ["any-arch", "cpp_demangle", "unarm", "arm-attr"]
|
||||
bindings = ["serde_json", "prost", "pbjson"]
|
||||
wasm = ["bindings", "console_error_panic_hook", "console_log"]
|
||||
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",
|
||||
]
|
||||
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",
|
||||
]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
features = ["all"]
|
||||
@@ -33,20 +121,20 @@ features = ["all"]
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
bimap = { version = "0.6", features = ["serde"], optional = true }
|
||||
byteorder = "1.5"
|
||||
filetime = "0.2"
|
||||
flagset = "0.4"
|
||||
log = "0.4"
|
||||
memmap2 = "0.9"
|
||||
num-traits = "0.2"
|
||||
byteorder = { version = "1.5", optional = true }
|
||||
filetime = { version = "0.2", optional = true }
|
||||
flagset = { version = "0.4", optional = true }
|
||||
log = { version = "0.4", optional = true }
|
||||
memmap2 = { version = "0.9", optional = true }
|
||||
num-traits = { version = "0.2", optional = true }
|
||||
object = { version = "0.36", features = ["read_core", "std", "elf", "pe"], default-features = false }
|
||||
pbjson = { version = "0.7", optional = true }
|
||||
prost = { version = "0.13", optional = true }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
similar = { version = "2.6", default-features = false }
|
||||
strum = { version = "0.26", features = ["derive"] }
|
||||
wasm-bindgen = "0.2"
|
||||
tsify-next = { version = "0.5", default-features = false, features = ["js"] }
|
||||
serde = { version = "1.0", features = ["derive"], optional = true }
|
||||
similar = { version = "2.6", default-features = false, optional = true }
|
||||
strum = { version = "0.26", features = ["derive"], optional = true }
|
||||
wasm-bindgen = { version = "0.2", optional = true }
|
||||
tsify-next = { version = "0.5", default-features = false, features = ["js"], optional = true }
|
||||
console_log = { version = "1.0", optional = true }
|
||||
console_error_panic_hook = { version = "0.1", optional = true }
|
||||
|
||||
@@ -76,6 +164,38 @@ msvc-demangler = { version = "0.10", optional = true }
|
||||
unarm = { version = "1.6", optional = true }
|
||||
arm-attr = { version = "0.1", optional = true }
|
||||
|
||||
# arm64
|
||||
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 = { git = "https://github.com/notify-rs/notify", rev = "128bf6230c03d39dbb7f301ff7b20e594e34c3a2", version = "6.1.1", optional = true }
|
||||
notify-debouncer-full = { git = "https://github.com/notify-rs/notify", rev = "128bf6230c03d39dbb7f301ff7b20e594e34c3a2", version = "0.4.0", optional = true }
|
||||
shell-escape = { version = "0.1", optional = true }
|
||||
tempfile = { version = "3.14", 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]
|
||||
prost-build = "0.13"
|
||||
pbjson-build = "0.7"
|
||||
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 }
|
||||
|
||||
@@ -11,4 +11,5 @@ objdiff-core contains the core functionality of [objdiff](https://github.com/enc
|
||||
- **`ppc`**: Enables the PowerPC backend powered by [ppc750cl](https://github.com/encounter/ppc750cl).
|
||||
- **`x86`**: Enables the x86 backend powered by [iced-x86](https://crates.io/crates/iced-x86).
|
||||
- **`arm`**: Enables the ARM backend powered by [unarm](https://github.com/AetiasHax/unarm).
|
||||
- **`arm64`**: Enables the ARM64 backend powered by [yaxpeax-arm](https://github.com/iximeow/yaxpeax-arm).
|
||||
- **`bindings`**: Enables serialization and deserialization of objdiff data structures.
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
#[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")]
|
||||
fn compile_protos() {
|
||||
use std::path::{Path, PathBuf};
|
||||
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("protos");
|
||||
let descriptor_path = root.join("proto_descriptor.bin");
|
||||
println!("cargo:rerun-if-changed={}", descriptor_path.display());
|
||||
|
||||
230
objdiff-core/config-schema.json
Normal file
230
objdiff-core/config-schema.json
Normal file
@@ -0,0 +1,230 @@
|
||||
{
|
||||
"properties": [
|
||||
{
|
||||
"id": "relaxRelocDiffs",
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"name": "Relax relocation diffs",
|
||||
"description": "Ignores differences in relocation targets. (Address, name, etc)"
|
||||
},
|
||||
{
|
||||
"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": [
|
||||
"relaxRelocDiffs",
|
||||
"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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
491
objdiff-core/config_gen.rs
Normal file
491
objdiff-core/config_gen.rs
Normal file
@@ -0,0 +1,491 @@
|
||||
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();
|
||||
}
|
||||
@@ -21,9 +21,9 @@ enum SymbolFlag {
|
||||
SYMBOL_NONE = 0;
|
||||
SYMBOL_GLOBAL = 1;
|
||||
SYMBOL_LOCAL = 2;
|
||||
SYMBOL_WEAK = 3;
|
||||
SYMBOL_COMMON = 4;
|
||||
SYMBOL_HIDDEN = 5;
|
||||
SYMBOL_WEAK = 4;
|
||||
SYMBOL_COMMON = 8;
|
||||
SYMBOL_HIDDEN = 16;
|
||||
}
|
||||
|
||||
// A single parsed instruction
|
||||
@@ -122,10 +122,17 @@ message InstructionBranchTo {
|
||||
uint32 branch_index = 2;
|
||||
}
|
||||
|
||||
message FunctionDiff {
|
||||
message SymbolRef {
|
||||
optional uint32 section_index = 1;
|
||||
uint32 symbol_index = 2;
|
||||
}
|
||||
|
||||
message SymbolDiff {
|
||||
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 {
|
||||
@@ -140,7 +147,7 @@ message SectionDiff {
|
||||
SectionKind kind = 2;
|
||||
uint64 size = 3;
|
||||
uint64 address = 4;
|
||||
repeated FunctionDiff functions = 5;
|
||||
repeated SymbolDiff symbols = 5;
|
||||
repeated DataDiff data = 6;
|
||||
optional float match_percent = 7;
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -124,11 +124,9 @@ impl ObjArch for ObjArchArm {
|
||||
.get(&SectionIndex(section_index))
|
||||
.map(|x| x.as_slice())
|
||||
.unwrap_or(&fallback_mappings);
|
||||
let first_mapping_idx =
|
||||
match mapping_symbols.binary_search_by_key(&start_addr, |x| x.address) {
|
||||
Ok(idx) => idx,
|
||||
Err(idx) => idx - 1,
|
||||
};
|
||||
let first_mapping_idx = mapping_symbols
|
||||
.binary_search_by_key(&start_addr, |x| x.address)
|
||||
.unwrap_or_else(|idx| idx - 1);
|
||||
let first_mapping = mapping_symbols[first_mapping_idx].mapping;
|
||||
|
||||
let mut mappings_iter =
|
||||
@@ -141,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,
|
||||
@@ -215,7 +213,7 @@ impl ObjArch for ObjArchArm {
|
||||
address: address as u64,
|
||||
size: (parser.address - address) as u8,
|
||||
op: ins.opcode_id(),
|
||||
mnemonic: parsed_ins.mnemonic.to_string(),
|
||||
mnemonic: Cow::Borrowed(parsed_ins.mnemonic),
|
||||
args,
|
||||
reloc,
|
||||
branch_dest,
|
||||
@@ -234,7 +232,7 @@ impl ObjArch for ObjArchArm {
|
||||
section: &ObjSection,
|
||||
address: u64,
|
||||
reloc: &Relocation,
|
||||
) -> anyhow::Result<i64> {
|
||||
) -> Result<i64> {
|
||||
let address = address as usize;
|
||||
Ok(match reloc.flags() {
|
||||
// ARM calls
|
||||
|
||||
2840
objdiff-core/src/arch/arm64.rs
Normal file
2840
objdiff-core/src/arch/arm64.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -83,7 +83,7 @@ impl ObjArch for ObjArchMips {
|
||||
&self,
|
||||
address: u64,
|
||||
code: &[u8],
|
||||
_section_index: usize,
|
||||
section_index: usize,
|
||||
relocations: &[ObjReloc],
|
||||
line_info: &BTreeMap<u64, u32>,
|
||||
config: &DiffObjConfig,
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -119,7 +119,7 @@ impl ObjArch for ObjArchMips {
|
||||
let op = instruction.unique_id as u16;
|
||||
ops.push(op);
|
||||
|
||||
let mnemonic = instruction.opcode_name().to_string();
|
||||
let mnemonic = instruction.opcode_name();
|
||||
let is_branch = instruction.is_branch();
|
||||
let branch_offset = instruction.branch_offset();
|
||||
let mut branch_dest = if is_branch {
|
||||
@@ -140,11 +140,18 @@ impl ObjArch for ObjArchMips {
|
||||
| OperandType::cpu_label
|
||||
| OperandType::cpu_branch_target_label => {
|
||||
if let Some(reloc) = reloc {
|
||||
if matches!(&reloc.target_section, Some(s) if s == ".text")
|
||||
&& reloc.target.address > start_address
|
||||
&& reloc.target.address < end_address
|
||||
// If the relocation target is within the current function, we can
|
||||
// convert it into a relative branch target. Note that we check
|
||||
// target_address > start_address instead of >= so that recursive
|
||||
// tail calls are not considered branch targets.
|
||||
let target_address =
|
||||
reloc.target.address.checked_add_signed(reloc.addend);
|
||||
if reloc.target.orig_section_index == Some(section_index)
|
||||
&& matches!(target_address, Some(addr) if addr > start_address && addr < end_address)
|
||||
{
|
||||
args.push(ObjInsArg::BranchDest(reloc.target.address));
|
||||
let target_address = target_address.unwrap();
|
||||
args.push(ObjInsArg::BranchDest(target_address));
|
||||
branch_dest = Some(target_address);
|
||||
} else {
|
||||
push_reloc(&mut args, reloc)?;
|
||||
branch_dest = None;
|
||||
@@ -195,7 +202,7 @@ impl ObjArch for ObjArchMips {
|
||||
address: cur_addr as u64,
|
||||
size: 4,
|
||||
op,
|
||||
mnemonic,
|
||||
mnemonic: Cow::Borrowed(mnemonic),
|
||||
args,
|
||||
reloc: reloc.cloned(),
|
||||
branch_dest,
|
||||
|
||||
@@ -12,6 +12,8 @@ use crate::{
|
||||
|
||||
#[cfg(feature = "arm")]
|
||||
mod arm;
|
||||
#[cfg(feature = "arm64")]
|
||||
mod arm64;
|
||||
#[cfg(feature = "mips")]
|
||||
pub mod mips;
|
||||
#[cfg(feature = "ppc")]
|
||||
@@ -35,8 +37,21 @@ 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.");
|
||||
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 => {
|
||||
@@ -80,10 +95,10 @@ impl DataType {
|
||||
}
|
||||
}
|
||||
DataType::Float => {
|
||||
format!("Float: {}", Endian::read_f32(bytes))
|
||||
format!("Float: {:?}f", Endian::read_f32(bytes))
|
||||
}
|
||||
DataType::Double => {
|
||||
format!("Double: {}", Endian::read_f64(bytes))
|
||||
format!("Double: {:?}", Endian::read_f64(bytes))
|
||||
}
|
||||
DataType::Bytes => {
|
||||
format!("Bytes: {:#?}", bytes)
|
||||
@@ -161,6 +176,8 @@ pub fn new_arch(object: &File) -> Result<Box<dyn ObjArch>> {
|
||||
Architecture::I386 | Architecture::X86_64 => Box::new(x86::ObjArchX86::new(object)?),
|
||||
#[cfg(feature = "arm")]
|
||||
Architecture::Arm => Box::new(arm::ObjArchArm::new(object)?),
|
||||
#[cfg(feature = "arm64")]
|
||||
Architecture::Aarch64 => Box::new(arm64::ObjArchArm64::new(object)?),
|
||||
arch => bail!("Unsupported architecture: {arch:?}"),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use std::{borrow::Cow, collections::BTreeMap};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
collections::{BTreeMap, HashMap},
|
||||
};
|
||||
|
||||
use anyhow::{bail, ensure, Result};
|
||||
use byteorder::BigEndian;
|
||||
@@ -7,7 +10,7 @@ use object::{
|
||||
elf, File, Object, ObjectSection, ObjectSymbol, Relocation, RelocationFlags, RelocationTarget,
|
||||
Symbol, SymbolKind,
|
||||
};
|
||||
use ppc750cl::{Argument, InsIter, Opcode, GPR};
|
||||
use ppc750cl::{Argument, InsIter, Opcode, ParsedIns, GPR};
|
||||
|
||||
use crate::{
|
||||
arch::{DataType, ObjArch, ProcessCodeResult},
|
||||
@@ -49,6 +52,8 @@ 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,9 +148,9 @@ impl ObjArch for ObjArchPpc {
|
||||
insts.push(ObjIns {
|
||||
address: cur_addr as u64,
|
||||
size: 4,
|
||||
mnemonic: simplified.mnemonic.to_string(),
|
||||
mnemonic: Cow::Borrowed(simplified.mnemonic),
|
||||
args,
|
||||
reloc: reloc.cloned(),
|
||||
reloc: reloc.or(fake_pool_reloc_for_addr.get(&cur_addr)).cloned(),
|
||||
op: ins.op as u16,
|
||||
branch_dest,
|
||||
line,
|
||||
@@ -173,6 +178,7 @@ 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"),
|
||||
@@ -188,26 +194,22 @@ impl ObjArch for ObjArchPpc {
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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,
|
||||
let op = Opcode::from(instruction.op as u8);
|
||||
if let Some(ty) = guess_data_type_from_load_store_inst_op(op) {
|
||||
Some(ty)
|
||||
} else if op == Opcode::Addi {
|
||||
// Assume that any addi instruction that references a local symbol is loading a string.
|
||||
// This hack is not ideal and results in tons of false positives where it will show
|
||||
// garbage strings (e.g. misinterpreting arrays, float literals, etc).
|
||||
// But not all strings are in the @stringBase pool, so the condition above that checks
|
||||
// the target symbol name would miss some.
|
||||
Some(DataType::String)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -381,3 +383,196 @@ 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))),
|
||||
(
|
||||
Opcode::Or,
|
||||
Argument::GPR(addr_dst_gpr),
|
||||
Argument::GPR(addr_src_gpr),
|
||||
Argument::None,
|
||||
) => Some((0, addr_src_gpr, Some(addr_dst_gpr))), // `mr` or `mr.`
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 orig_section_index = pool_reloc.target.orig_section_index?;
|
||||
// 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 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.
|
||||
let fake_target_symbol = 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: Some(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.
|
||||
let fake_addend = 0;
|
||||
Some(ObjReloc {
|
||||
flags: RelocationFlags::Elf { r_type: elf::R_PPC_NONE },
|
||||
address: cur_addr as u64,
|
||||
target: fake_target_symbol,
|
||||
addend: fake_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.
|
||||
// Limitations: This method currently only goes through the instructions in a function in linear
|
||||
// order, from start to finish. It does *not* follow any branches. This means that it could have
|
||||
// false positives or false negatives in determining which relocation is currently loaded in which
|
||||
// register at any given point in the function, as control flow is not respected.
|
||||
// There are currently no known examples of this method producing inaccurate results in reality, but
|
||||
// if examples are found, it may be possible to update this method to also follow all branches so
|
||||
// that it produces more accurate results.
|
||||
fn generate_fake_pool_reloc_for_addr_mapping(
|
||||
address: u64,
|
||||
code: &[u8],
|
||||
relocations: &[ObjReloc],
|
||||
) -> HashMap<u32, ObjReloc> {
|
||||
let mut active_pool_relocs = HashMap::new();
|
||||
let mut pool_reloc_for_addr = HashMap::new();
|
||||
for (cur_addr, ins) in InsIter::new(code, address as u32) {
|
||||
let simplified = ins.simplified();
|
||||
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]) {
|
||||
(
|
||||
Opcode::Addi,
|
||||
Argument::GPR(addr_dst_gpr),
|
||||
Argument::GPR(_addr_src_gpr),
|
||||
Argument::Simm(_simm),
|
||||
) => {
|
||||
active_pool_relocs.insert(addr_dst_gpr.0, reloc.clone()); // `lis` + `addi`
|
||||
}
|
||||
(
|
||||
Opcode::Ori,
|
||||
Argument::GPR(addr_dst_gpr),
|
||||
Argument::GPR(_addr_src_gpr),
|
||||
Argument::Uimm(_uimm),
|
||||
) => {
|
||||
active_pool_relocs.insert(addr_dst_gpr.0, reloc.clone()); // `lis` + `ori`
|
||||
}
|
||||
(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.
|
||||
active_pool_relocs.remove(&0);
|
||||
for gpr in 3..12 {
|
||||
active_pool_relocs.remove(&gpr);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
} 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) = active_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;
|
||||
active_pool_relocs.insert(addr_dst_gpr.0, new_reloc);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pool_reloc_for_addr
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ impl ObjArch for ObjArchX86 {
|
||||
address: 0,
|
||||
size: 0,
|
||||
op: 0,
|
||||
mnemonic: String::new(),
|
||||
mnemonic: Cow::Borrowed("<invalid>"),
|
||||
args: vec![],
|
||||
reloc: None,
|
||||
branch_dest: None,
|
||||
@@ -76,7 +76,7 @@ impl ObjArch for ObjArchX86 {
|
||||
address,
|
||||
size: instruction.len() as u8,
|
||||
op,
|
||||
mnemonic: String::new(),
|
||||
mnemonic: Cow::Borrowed("<invalid>"),
|
||||
args: vec![],
|
||||
reloc: reloc.cloned(),
|
||||
branch_dest: None,
|
||||
@@ -242,7 +242,8 @@ impl FormatterOutput for InstructionFormatterOutput {
|
||||
|
||||
fn write_mnemonic(&mut self, _instruction: &Instruction, text: &str) {
|
||||
self.formatted.push_str(text);
|
||||
self.ins.mnemonic = text.to_string();
|
||||
// TODO: can iced-x86 guarantee 'static here?
|
||||
self.ins.mnemonic = Cow::Owned(text.to_string());
|
||||
}
|
||||
|
||||
fn write_number(
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
#![allow(clippy::needless_lifetimes)] // Generated serde code
|
||||
use crate::{
|
||||
diff::{
|
||||
ObjDataDiff, ObjDataDiffKind, ObjDiff, ObjInsArgDiff, ObjInsBranchFrom, ObjInsBranchTo,
|
||||
ObjInsDiff, ObjInsDiffKind, ObjSectionDiff, ObjSymbolDiff,
|
||||
},
|
||||
obj,
|
||||
obj::{
|
||||
ObjInfo, ObjIns, ObjInsArg, ObjInsArgValue, ObjReloc, ObjSectionKind, ObjSymbol,
|
||||
ObjSymbolFlagSet, ObjSymbolFlags,
|
||||
@@ -38,14 +40,14 @@ impl ObjectDiff {
|
||||
impl SectionDiff {
|
||||
pub fn new(obj: &ObjInfo, section_index: usize, section_diff: &ObjSectionDiff) -> Self {
|
||||
let section = &obj.sections[section_index];
|
||||
let functions = section_diff.symbols.iter().map(|d| FunctionDiff::new(obj, d)).collect();
|
||||
let symbols = section_diff.symbols.iter().map(|d| SymbolDiff::new(obj, d)).collect();
|
||||
let data = section_diff.data_diff.iter().map(|d| DataDiff::new(obj, d)).collect();
|
||||
Self {
|
||||
name: section.name.to_string(),
|
||||
kind: SectionKind::from(section.kind) as i32,
|
||||
size: section.size,
|
||||
address: section.address,
|
||||
functions,
|
||||
symbols,
|
||||
data,
|
||||
match_percent: section_diff.match_percent,
|
||||
}
|
||||
@@ -63,19 +65,32 @@ impl From<ObjSectionKind> for SectionKind {
|
||||
}
|
||||
}
|
||||
|
||||
impl FunctionDiff {
|
||||
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 {
|
||||
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().map(InstructionDiff::from).collect();
|
||||
let instructions = symbol_diff
|
||||
.instructions
|
||||
.iter()
|
||||
.map(|ins_diff| InstructionDiff::new(object, ins_diff))
|
||||
.collect();
|
||||
Self {
|
||||
symbol: Some(Symbol::from(symbol)),
|
||||
// diff_symbol,
|
||||
symbol: Some(Symbol::new(symbol)),
|
||||
instructions,
|
||||
match_percent: symbol_diff.match_percent,
|
||||
target: symbol_diff.target_symbol.map(SymbolRef::from),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -90,8 +105,8 @@ impl DataDiff {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ObjSymbol> for Symbol {
|
||||
fn from(value: &'a ObjSymbol) -> Self {
|
||||
impl Symbol {
|
||||
pub fn new(value: &ObjSymbol) -> Self {
|
||||
Self {
|
||||
name: value.name.to_string(),
|
||||
demangled_name: value.demangled_name.clone(),
|
||||
@@ -105,7 +120,7 @@ impl<'a> From<&'a ObjSymbol> for Symbol {
|
||||
fn symbol_flags(value: ObjSymbolFlagSet) -> u32 {
|
||||
let mut flags = 0u32;
|
||||
if value.0.contains(ObjSymbolFlags::Global) {
|
||||
flags |= SymbolFlag::SymbolNone as u32;
|
||||
flags |= SymbolFlag::SymbolGlobal as u32;
|
||||
}
|
||||
if value.0.contains(ObjSymbolFlags::Local) {
|
||||
flags |= SymbolFlag::SymbolLocal as u32;
|
||||
@@ -122,29 +137,29 @@ fn symbol_flags(value: ObjSymbolFlagSet) -> u32 {
|
||||
flags
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ObjIns> for Instruction {
|
||||
fn from(value: &'a ObjIns) -> Self {
|
||||
impl Instruction {
|
||||
pub fn new(object: &ObjInfo, instruction: &ObjIns) -> Self {
|
||||
Self {
|
||||
address: value.address,
|
||||
size: value.size as u32,
|
||||
opcode: value.op as u32,
|
||||
mnemonic: value.mnemonic.clone(),
|
||||
formatted: value.formatted.clone(),
|
||||
arguments: value.args.iter().map(Argument::from).collect(),
|
||||
relocation: value.reloc.as_ref().map(Relocation::from),
|
||||
branch_dest: value.branch_dest,
|
||||
line_number: value.line,
|
||||
original: value.orig.clone(),
|
||||
address: instruction.address,
|
||||
size: instruction.size as u32,
|
||||
opcode: instruction.op as u32,
|
||||
mnemonic: instruction.mnemonic.to_string(),
|
||||
formatted: instruction.formatted.clone(),
|
||||
arguments: instruction.args.iter().map(Argument::new).collect(),
|
||||
relocation: instruction.reloc.as_ref().map(|reloc| Relocation::new(object, reloc)),
|
||||
branch_dest: instruction.branch_dest,
|
||||
line_number: instruction.line,
|
||||
original: instruction.orig.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ObjInsArg> for Argument {
|
||||
fn from(value: &'a ObjInsArg) -> Self {
|
||||
impl Argument {
|
||||
pub fn new(value: &ObjInsArg) -> Self {
|
||||
Self {
|
||||
value: Some(match value {
|
||||
ObjInsArg::PlainText(s) => argument::Value::PlainText(s.to_string()),
|
||||
ObjInsArg::Arg(v) => argument::Value::Argument(ArgumentValue::from(v)),
|
||||
ObjInsArg::Arg(v) => argument::Value::Argument(ArgumentValue::new(v)),
|
||||
ObjInsArg::Reloc => argument::Value::Relocation(ArgumentRelocation {}),
|
||||
ObjInsArg::BranchDest(dest) => argument::Value::BranchDest(*dest),
|
||||
}),
|
||||
@@ -152,8 +167,8 @@ impl<'a> From<&'a ObjInsArg> for Argument {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ObjInsArgValue> for ArgumentValue {
|
||||
fn from(value: &ObjInsArgValue) -> Self {
|
||||
impl ArgumentValue {
|
||||
pub fn new(value: &ObjInsArgValue) -> Self {
|
||||
Self {
|
||||
value: Some(match value {
|
||||
ObjInsArgValue::Signed(v) => argument_value::Value::Signed(*v),
|
||||
@@ -164,42 +179,39 @@ impl From<&ObjInsArgValue> for ArgumentValue {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ObjReloc> for Relocation {
|
||||
fn from(value: &ObjReloc) -> Self {
|
||||
impl Relocation {
|
||||
pub fn new(object: &ObjInfo, reloc: &ObjReloc) -> Self {
|
||||
Self {
|
||||
r#type: match value.flags {
|
||||
r#type: match reloc.flags {
|
||||
object::RelocationFlags::Elf { r_type } => r_type,
|
||||
object::RelocationFlags::MachO { r_type, .. } => r_type as u32,
|
||||
object::RelocationFlags::Coff { typ } => typ as u32,
|
||||
object::RelocationFlags::Xcoff { r_rtype, .. } => r_rtype as u32,
|
||||
_ => unreachable!(),
|
||||
},
|
||||
type_name: String::new(), // TODO
|
||||
target: Some(RelocationTarget::from(&value.target)),
|
||||
type_name: object.arch.display_reloc(reloc.flags).into_owned(),
|
||||
target: Some(RelocationTarget {
|
||||
symbol: Some(Symbol::new(&reloc.target)),
|
||||
addend: reloc.addend,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ObjSymbol> for RelocationTarget {
|
||||
fn from(value: &'a ObjSymbol) -> Self {
|
||||
Self { symbol: Some(Symbol::from(value)), addend: value.addend }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ObjInsDiff> for InstructionDiff {
|
||||
fn from(value: &'a ObjInsDiff) -> Self {
|
||||
impl InstructionDiff {
|
||||
pub fn new(object: &ObjInfo, instruction_diff: &ObjInsDiff) -> Self {
|
||||
Self {
|
||||
instruction: value.ins.as_ref().map(Instruction::from),
|
||||
diff_kind: DiffKind::from(value.kind) as i32,
|
||||
branch_from: value.branch_from.as_ref().map(InstructionBranchFrom::from),
|
||||
branch_to: value.branch_to.as_ref().map(InstructionBranchTo::from),
|
||||
arg_diff: value.arg_diff.iter().map(ArgumentDiff::from).collect(),
|
||||
instruction: instruction_diff.ins.as_ref().map(|ins| Instruction::new(object, ins)),
|
||||
diff_kind: DiffKind::from(instruction_diff.kind) as i32,
|
||||
branch_from: instruction_diff.branch_from.as_ref().map(InstructionBranchFrom::new),
|
||||
branch_to: instruction_diff.branch_to.as_ref().map(InstructionBranchTo::new),
|
||||
arg_diff: instruction_diff.arg_diff.iter().map(ArgumentDiff::new).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Option<ObjInsArgDiff>> for ArgumentDiff {
|
||||
fn from(value: &Option<ObjInsArgDiff>) -> Self {
|
||||
impl ArgumentDiff {
|
||||
pub fn new(value: &Option<ObjInsArgDiff>) -> Self {
|
||||
Self { diff_index: value.as_ref().map(|v| v.idx as u32) }
|
||||
}
|
||||
}
|
||||
@@ -228,8 +240,8 @@ impl From<ObjDataDiffKind> for DiffKind {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ObjInsBranchFrom> for InstructionBranchFrom {
|
||||
fn from(value: &'a ObjInsBranchFrom) -> Self {
|
||||
impl InstructionBranchFrom {
|
||||
pub fn new(value: &ObjInsBranchFrom) -> Self {
|
||||
Self {
|
||||
instruction_index: value.ins_idx.iter().map(|&x| x as u32).collect(),
|
||||
branch_index: value.branch_idx as u32,
|
||||
@@ -237,8 +249,8 @@ impl<'a> From<&'a ObjInsBranchFrom> for InstructionBranchFrom {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ObjInsBranchTo> for InstructionBranchTo {
|
||||
fn from(value: &'a ObjInsBranchTo) -> Self {
|
||||
impl InstructionBranchTo {
|
||||
pub fn new(value: &ObjInsBranchTo) -> Self {
|
||||
Self { instruction_index: value.ins_idx as u32, branch_index: value.branch_idx as u32 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#![allow(clippy::needless_lifetimes)] // Generated serde code
|
||||
use std::ops::AddAssign;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
@@ -173,8 +174,7 @@ impl Report {
|
||||
continue;
|
||||
}
|
||||
fn is_sub_category(id: &str, parent: &str, sep: char) -> bool {
|
||||
id.starts_with(parent)
|
||||
&& id.get(parent.len()..).map_or(false, |s| s.starts_with(sep))
|
||||
id.starts_with(parent) && id.get(parent.len()..).is_some_and(|s| s.starts_with(sep))
|
||||
}
|
||||
let mut sub_categories = self
|
||||
.categories
|
||||
|
||||
@@ -13,20 +13,22 @@ fn parse_object(
|
||||
fn parse_and_run_diff(
|
||||
left: Option<Box<[u8]>>,
|
||||
right: Option<Box<[u8]>>,
|
||||
config: diff::DiffObjConfig,
|
||||
diff_config: diff::DiffObjConfig,
|
||||
mapping_config: diff::MappingConfig,
|
||||
) -> Result<DiffResult, JsError> {
|
||||
let target = parse_object(left, &config)?;
|
||||
let base = parse_object(right, &config)?;
|
||||
run_diff(target.as_ref(), base.as_ref(), config)
|
||||
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)
|
||||
}
|
||||
|
||||
fn run_diff(
|
||||
left: Option<&obj::ObjInfo>,
|
||||
right: Option<&obj::ObjInfo>,
|
||||
config: diff::DiffObjConfig,
|
||||
diff_config: diff::DiffObjConfig,
|
||||
mapping_config: diff::MappingConfig,
|
||||
) -> Result<DiffResult, JsError> {
|
||||
log::debug!("Running diff with config: {:?}", config);
|
||||
let result = diff::diff_objs(&config, left, right, None).to_js()?;
|
||||
log::debug!("Running diff with config: {:?}", diff_config);
|
||||
let result = diff::diff_objs(&diff_config, &mapping_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))
|
||||
@@ -46,9 +48,10 @@ fn run_diff(
|
||||
pub fn run_diff_proto(
|
||||
left: Option<Box<[u8]>>,
|
||||
right: Option<Box<[u8]>>,
|
||||
config: diff::DiffObjConfig,
|
||||
diff_config: diff::DiffObjConfig,
|
||||
mapping_config: diff::MappingConfig,
|
||||
) -> Result<Box<[u8]>, JsError> {
|
||||
let out = parse_and_run_diff(left, right, config)?;
|
||||
let out = parse_and_run_diff(left, right, diff_config, mapping_config)?;
|
||||
Ok(out.encode_to_vec().into_boxed_slice())
|
||||
}
|
||||
|
||||
|
||||
106
objdiff-core/src/build/mod.rs
Normal file
106
objdiff-core/src/build/mod.rs
Normal file
@@ -0,0 +1,106 @@
|
||||
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 }
|
||||
}
|
||||
75
objdiff-core/src/build/watcher.rs
Normal file
75
objdiff-core/src/build/watcher.rs
Normal file
@@ -0,0 +1,75 @@
|
||||
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)
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
fs,
|
||||
fs::File,
|
||||
io::{BufReader, BufWriter, Read},
|
||||
@@ -6,11 +7,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>,
|
||||
@@ -27,7 +28,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<Glob>>,
|
||||
pub watch_patterns: Option<Vec<String>>,
|
||||
#[serde(default, alias = "objects", skip_serializing_if = "Option::is_none")]
|
||||
pub units: Option<Vec<ProjectObject>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
@@ -52,9 +53,21 @@ 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>,
|
||||
@@ -78,9 +91,11 @@ pub struct ProjectObject {
|
||||
pub symbol_mappings: Option<SymbolMappings>,
|
||||
}
|
||||
|
||||
pub type SymbolMappings = BiBTreeMap<String, String>;
|
||||
#[cfg_attr(feature = "wasm", tsify_next::declare)]
|
||||
pub type SymbolMappings = BTreeMap<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>,
|
||||
@@ -95,6 +110,7 @@ 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,
|
||||
@@ -154,6 +170,7 @@ 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>,
|
||||
@@ -165,6 +182,8 @@ pub struct ScratchConfig {
|
||||
pub ctx_path: Option<PathBuf>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub build_ctx: Option<bool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub preset_id: Option<u32>,
|
||||
}
|
||||
|
||||
pub const CONFIG_FILENAMES: [&str; 3] = ["objdiff.json", "objdiff.yml", "objdiff.yaml"];
|
||||
@@ -174,6 +193,10 @@ 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,
|
||||
|
||||
@@ -9,7 +9,7 @@ use crate::{
|
||||
DiffObjConfig, ObjInsArgDiff, ObjInsBranchFrom, ObjInsBranchTo, ObjInsDiff, ObjInsDiffKind,
|
||||
ObjSymbolDiff,
|
||||
},
|
||||
obj::{ObjInfo, ObjInsArg, ObjReloc, ObjSymbol, ObjSymbolFlags, SymbolRef},
|
||||
obj::{ObjInfo, ObjInsArg, ObjReloc, ObjSection, ObjSymbol, ObjSymbolFlags, SymbolRef},
|
||||
};
|
||||
|
||||
pub fn process_code_symbol(
|
||||
@@ -21,14 +21,30 @@ pub fn process_code_symbol(
|
||||
let section = section.ok_or_else(|| anyhow!("Code symbol section not found"))?;
|
||||
let code = §ion.data
|
||||
[symbol.section_address as usize..(symbol.section_address + symbol.size) as usize];
|
||||
obj.arch.process_code(
|
||||
let mut res = obj.arch.process_code(
|
||||
symbol.address,
|
||||
code,
|
||||
section.orig_index,
|
||||
§ion.relocations,
|
||||
§ion.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> {
|
||||
@@ -45,6 +61,8 @@ pub fn no_diff_code(out: &ProcessCodeResult, symbol_ref: SymbolRef) -> Result<Ob
|
||||
}
|
||||
|
||||
pub fn diff_code(
|
||||
left_obj: &ObjInfo,
|
||||
right_obj: &ObjInfo,
|
||||
left_out: &ProcessCodeResult,
|
||||
right_out: &ProcessCodeResult,
|
||||
left_symbol_ref: SymbolRef,
|
||||
@@ -60,7 +78,7 @@ pub fn diff_code(
|
||||
|
||||
let mut diff_state = InsDiffState::default();
|
||||
for (left, right) in left_diff.iter_mut().zip(right_diff.iter_mut()) {
|
||||
let result = compare_ins(config, left, right, &mut diff_state)?;
|
||||
let result = compare_ins(config, left_obj, right_obj, left, right, &mut diff_state)?;
|
||||
left.kind = result.kind;
|
||||
right.kind = result.kind;
|
||||
left.arg_diff = result.left_args_diff;
|
||||
@@ -170,12 +188,33 @@ fn resolve_branches(vec: &mut [ObjInsDiff]) {
|
||||
}
|
||||
}
|
||||
|
||||
fn address_eq(left: &ObjSymbol, right: &ObjSymbol) -> bool {
|
||||
left.address as i64 + left.addend == right.address as i64 + right.addend
|
||||
fn address_eq(left: &ObjReloc, right: &ObjReloc) -> bool {
|
||||
left.target.address as i64 + left.addend == right.target.address as i64 + right.addend
|
||||
}
|
||||
|
||||
fn section_name_eq(
|
||||
left_obj: &ObjInfo,
|
||||
right_obj: &ObjInfo,
|
||||
left_orig_section_index: usize,
|
||||
right_orig_section_index: usize,
|
||||
) -> bool {
|
||||
let Some(left_section) =
|
||||
left_obj.sections.iter().find(|s| s.orig_index == left_orig_section_index)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
let Some(right_section) =
|
||||
right_obj.sections.iter().find(|s| s.orig_index == right_orig_section_index)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
left_section.name == right_section.name
|
||||
}
|
||||
|
||||
fn reloc_eq(
|
||||
config: &DiffObjConfig,
|
||||
left_obj: &ObjInfo,
|
||||
right_obj: &ObjInfo,
|
||||
left_reloc: Option<&ObjReloc>,
|
||||
right_reloc: Option<&ObjReloc>,
|
||||
) -> bool {
|
||||
@@ -189,23 +228,26 @@ fn reloc_eq(
|
||||
return true;
|
||||
}
|
||||
|
||||
let name_matches = left.target.name == right.target.name;
|
||||
match (&left.target_section, &right.target_section) {
|
||||
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 or address match
|
||||
sl == sr && (name_matches || address_eq(&left.target, &right.target))
|
||||
section_name_eq(left_obj, right_obj, *sl, *sr)
|
||||
&& (symbol_name_matches || address_eq(left, right))
|
||||
}
|
||||
(Some(_), None) => false,
|
||||
(None, Some(_)) => {
|
||||
// Match if possibly stripped weak symbol
|
||||
name_matches && right.target.flags.0.contains(ObjSymbolFlags::Weak)
|
||||
symbol_name_matches && right.target.flags.0.contains(ObjSymbolFlags::Weak)
|
||||
}
|
||||
(None, None) => name_matches,
|
||||
(None, None) => symbol_name_matches,
|
||||
}
|
||||
}
|
||||
|
||||
fn arg_eq(
|
||||
config: &DiffObjConfig,
|
||||
left_obj: &ObjInfo,
|
||||
right_obj: &ObjInfo,
|
||||
left: &ObjInsArg,
|
||||
right: &ObjInsArg,
|
||||
left_diff: &ObjInsDiff,
|
||||
@@ -217,7 +259,7 @@ fn arg_eq(
|
||||
_ => false,
|
||||
},
|
||||
ObjInsArg::Arg(l) => match right {
|
||||
ObjInsArg::Arg(r) => l == r,
|
||||
ObjInsArg::Arg(r) => l.loose_eq(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.relax_reloc_diffs,
|
||||
@@ -227,15 +269,23 @@ fn arg_eq(
|
||||
matches!(right, ObjInsArg::Reloc)
|
||||
&& reloc_eq(
|
||||
config,
|
||||
left_obj,
|
||||
right_obj,
|
||||
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(_) => {
|
||||
ObjInsArg::BranchDest(_) => match right {
|
||||
// Compare dest instruction idx after diffing
|
||||
left_diff.branch_to.as_ref().map(|b| b.ins_idx)
|
||||
== right_diff.branch_to.as_ref().map(|b| b.ins_idx)
|
||||
}
|
||||
ObjInsArg::BranchDest(_) => {
|
||||
left_diff.branch_to.as_ref().map(|b| b.ins_idx)
|
||||
== right_diff.branch_to.as_ref().map(|b| b.ins_idx)
|
||||
}
|
||||
// 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.relax_reloc_diffs,
|
||||
_ => false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,21 +307,18 @@ struct InsDiffResult {
|
||||
|
||||
fn compare_ins(
|
||||
config: &DiffObjConfig,
|
||||
left_obj: &ObjInfo,
|
||||
right_obj: &ObjInfo,
|
||||
left: &ObjInsDiff,
|
||||
right: &ObjInsDiff,
|
||||
state: &mut InsDiffState,
|
||||
) -> Result<InsDiffResult> {
|
||||
let mut result = InsDiffResult::default();
|
||||
if let (Some(left_ins), Some(right_ins)) = (&left.ins, &right.ins) {
|
||||
if left_ins.args.len() != right_ins.args.len()
|
||||
|| left_ins.op != right_ins.op
|
||||
// Check if any PlainText segments differ (punctuation and spacing)
|
||||
// This indicates a more significant difference than a simple arg mismatch
|
||||
|| !left_ins.args.iter().zip(&right_ins.args).all(|(a, b)| match (a, b) {
|
||||
(ObjInsArg::PlainText(l), ObjInsArg::PlainText(r)) => l == r,
|
||||
_ => true,
|
||||
})
|
||||
{
|
||||
// Count only non-PlainText args
|
||||
let left_args_count = left_ins.iter_args().count();
|
||||
let right_args_count = right_ins.iter_args().count();
|
||||
if left_args_count != right_args_count || left_ins.op != right_ins.op {
|
||||
// Totally different op
|
||||
result.kind = ObjInsDiffKind::Replace;
|
||||
state.diff_count += 1;
|
||||
@@ -282,8 +329,8 @@ fn compare_ins(
|
||||
result.kind = ObjInsDiffKind::OpMismatch;
|
||||
state.diff_count += 1;
|
||||
}
|
||||
for (a, b) in left_ins.args.iter().zip(&right_ins.args) {
|
||||
if arg_eq(config, a, b, left, right) {
|
||||
for (a, b) in left_ins.iter_args().zip(right_ins.iter_args()) {
|
||||
if arg_eq(config, left_obj, right_obj, a, b, left, right) {
|
||||
result.left_args_diff.push(None);
|
||||
result.right_args_diff.push(None);
|
||||
} else {
|
||||
@@ -294,8 +341,11 @@ fn compare_ins(
|
||||
let a_str = match a {
|
||||
ObjInsArg::PlainText(arg) => arg.to_string(),
|
||||
ObjInsArg::Arg(arg) => arg.to_string(),
|
||||
ObjInsArg::Reloc => String::new(),
|
||||
ObjInsArg::BranchDest(arg) => format!("{arg}"),
|
||||
ObjInsArg::Reloc => left_ins
|
||||
.reloc
|
||||
.as_ref()
|
||||
.map_or_else(|| "<unknown>".to_string(), |r| r.target.name.clone()),
|
||||
ObjInsArg::BranchDest(arg) => arg.to_string(),
|
||||
};
|
||||
let a_diff = if let Some(idx) = state.left_args_idx.get(&a_str) {
|
||||
ObjInsArgDiff { idx: *idx }
|
||||
@@ -308,8 +358,11 @@ fn compare_ins(
|
||||
let b_str = match b {
|
||||
ObjInsArg::PlainText(arg) => arg.to_string(),
|
||||
ObjInsArg::Arg(arg) => arg.to_string(),
|
||||
ObjInsArg::Reloc => String::new(),
|
||||
ObjInsArg::BranchDest(arg) => format!("{arg}"),
|
||||
ObjInsArg::Reloc => right_ins
|
||||
.reloc
|
||||
.as_ref()
|
||||
.map_or_else(|| "<unknown>".to_string(), |r| r.target.name.clone()),
|
||||
ObjInsArg::BranchDest(arg) => arg.to_string(),
|
||||
};
|
||||
let b_diff = if let Some(idx) = state.right_args_idx.get(&b_str) {
|
||||
ObjInsArgDiff { idx: *idx }
|
||||
@@ -332,3 +385,16 @@ 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())
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ pub enum DiffText<'a> {
|
||||
/// Branch destination
|
||||
BranchDest(u64, Option<&'a ObjInsArgDiff>),
|
||||
/// Symbol name
|
||||
Symbol(&'a ObjSymbol),
|
||||
Symbol(&'a ObjSymbol, Option<&'a ObjInsArgDiff>),
|
||||
/// Number of spaces
|
||||
Spacing(usize),
|
||||
/// End of line
|
||||
@@ -58,20 +58,23 @@ pub fn display_diff<E>(
|
||||
cb(DiffText::Spacing(4))?;
|
||||
}
|
||||
cb(DiffText::Opcode(&ins.mnemonic, ins.op))?;
|
||||
let mut arg_diff_idx = 0; // non-PlainText index
|
||||
for (i, arg) in ins.args.iter().enumerate() {
|
||||
if i == 0 {
|
||||
cb(DiffText::Spacing(1))?;
|
||||
}
|
||||
let diff = ins_diff.arg_diff.get(i).and_then(|o| o.as_ref());
|
||||
let diff = ins_diff.arg_diff.get(arg_diff_idx).and_then(|o| o.as_ref());
|
||||
match arg {
|
||||
ObjInsArg::PlainText(s) => {
|
||||
cb(DiffText::Basic(s))?;
|
||||
}
|
||||
ObjInsArg::Arg(v) => {
|
||||
cb(DiffText::Argument(v, diff))?;
|
||||
arg_diff_idx += 1;
|
||||
}
|
||||
ObjInsArg::Reloc => {
|
||||
display_reloc_name(ins.reloc.as_ref().unwrap(), &mut cb)?;
|
||||
display_reloc_name(ins.reloc.as_ref().unwrap(), &mut cb, diff)?;
|
||||
arg_diff_idx += 1;
|
||||
}
|
||||
ObjInsArg::BranchDest(dest) => {
|
||||
if let Some(dest) = dest.checked_sub(base_addr) {
|
||||
@@ -79,6 +82,7 @@ pub fn display_diff<E>(
|
||||
} else {
|
||||
cb(DiffText::Basic("<unknown>"))?;
|
||||
}
|
||||
arg_diff_idx += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -92,11 +96,12 @@ pub fn display_diff<E>(
|
||||
fn display_reloc_name<E>(
|
||||
reloc: &ObjReloc,
|
||||
mut cb: impl FnMut(DiffText) -> Result<(), E>,
|
||||
diff: Option<&ObjInsArgDiff>,
|
||||
) -> Result<(), E> {
|
||||
cb(DiffText::Symbol(&reloc.target))?;
|
||||
match reloc.target.addend.cmp(&0i64) {
|
||||
Ordering::Greater => cb(DiffText::Basic(&format!("+{:#x}", reloc.target.addend))),
|
||||
Ordering::Less => cb(DiffText::Basic(&format!("-{:#x}", -reloc.target.addend))),
|
||||
cb(DiffText::Symbol(&reloc.target, diff))?;
|
||||
match reloc.addend.cmp(&0i64) {
|
||||
Ordering::Greater => cb(DiffText::Basic(&format!("+{:#x}", reloc.addend))),
|
||||
Ordering::Less => cb(DiffText::Basic(&format!("-{:#x}", -reloc.addend))),
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
@@ -106,7 +111,7 @@ impl PartialEq<DiffText<'_>> for HighlightKind {
|
||||
match (self, other) {
|
||||
(HighlightKind::Opcode(a), DiffText::Opcode(_, b)) => a == b,
|
||||
(HighlightKind::Arg(a), DiffText::Argument(b, _)) => a.loose_eq(b),
|
||||
(HighlightKind::Symbol(a), DiffText::Symbol(b)) => a == &b.name,
|
||||
(HighlightKind::Symbol(a), DiffText::Symbol(b, _)) => a == &b.name,
|
||||
(HighlightKind::Address(a), DiffText::Address(b) | DiffText::BranchDest(b, _)) => {
|
||||
a == b
|
||||
}
|
||||
@@ -124,7 +129,7 @@ impl From<DiffText<'_>> for HighlightKind {
|
||||
match value {
|
||||
DiffText::Opcode(_, op) => HighlightKind::Opcode(op),
|
||||
DiffText::Argument(arg, _) => HighlightKind::Arg(arg.clone()),
|
||||
DiffText::Symbol(sym) => HighlightKind::Symbol(sym.name.to_string()),
|
||||
DiffText::Symbol(sym, _) => HighlightKind::Symbol(sym.name.to_string()),
|
||||
DiffText::Address(addr) | DiffText::BranchDest(addr, _) => HighlightKind::Address(addr),
|
||||
_ => HighlightKind::None,
|
||||
}
|
||||
|
||||
@@ -11,194 +11,14 @@ use crate::{
|
||||
diff_generic_section, no_diff_symbol,
|
||||
},
|
||||
},
|
||||
obj::{ObjInfo, ObjIns, ObjSection, ObjSectionKind, ObjSymbol, SymbolRef},
|
||||
obj::{ObjInfo, ObjIns, ObjSection, ObjSectionKind, ObjSymbol, SymbolRef, SECTION_COMMON},
|
||||
};
|
||||
|
||||
pub mod code;
|
||||
pub mod data;
|
||||
pub mod display;
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
Copy,
|
||||
Clone,
|
||||
Default,
|
||||
Eq,
|
||||
PartialEq,
|
||||
serde::Deserialize,
|
||||
serde::Serialize,
|
||||
strum::VariantArray,
|
||||
strum::EnumMessage,
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
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, tsify_next::Tsify)]
|
||||
#[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,
|
||||
}
|
||||
}
|
||||
}
|
||||
include!(concat!(env!("OUT_DIR"), "/config.gen.rs"));
|
||||
|
||||
impl DiffObjConfig {
|
||||
pub fn separator(&self) -> &'static str {
|
||||
@@ -244,7 +64,7 @@ pub struct ObjInsDiff {
|
||||
pub branch_from: Option<ObjInsBranchFrom>,
|
||||
/// Branches to instruction
|
||||
pub branch_to: Option<ObjInsBranchTo>,
|
||||
/// Arg diffs
|
||||
/// Arg diffs (only contains non-PlainText args)
|
||||
pub arg_diff: Vec<Option<ObjInsArgDiff>>,
|
||||
}
|
||||
|
||||
@@ -339,7 +159,7 @@ impl ObjDiff {
|
||||
}
|
||||
for (symbol_idx, _) in obj.common.iter().enumerate() {
|
||||
result.common.push(ObjSymbolDiff {
|
||||
symbol_ref: SymbolRef { section_idx: obj.sections.len(), symbol_idx },
|
||||
symbol_ref: SymbolRef { section_idx: SECTION_COMMON, symbol_idx },
|
||||
target_symbol: None,
|
||||
instructions: vec![],
|
||||
match_percent: None,
|
||||
@@ -360,7 +180,7 @@ impl ObjDiff {
|
||||
|
||||
#[inline]
|
||||
pub fn symbol_diff(&self, symbol_ref: SymbolRef) -> &ObjSymbolDiff {
|
||||
if symbol_ref.section_idx == self.sections.len() {
|
||||
if symbol_ref.section_idx == SECTION_COMMON {
|
||||
&self.common[symbol_ref.symbol_idx]
|
||||
} else {
|
||||
&self.section_diff(symbol_ref.section_idx).symbols[symbol_ref.symbol_idx]
|
||||
@@ -369,7 +189,7 @@ impl ObjDiff {
|
||||
|
||||
#[inline]
|
||||
pub fn symbol_diff_mut(&mut self, symbol_ref: SymbolRef) -> &mut ObjSymbolDiff {
|
||||
if symbol_ref.section_idx == self.sections.len() {
|
||||
if symbol_ref.section_idx == SECTION_COMMON {
|
||||
&mut self.common[symbol_ref.symbol_idx]
|
||||
} else {
|
||||
&mut self.section_diff_mut(symbol_ref.section_idx).symbols[symbol_ref.symbol_idx]
|
||||
@@ -385,12 +205,13 @@ pub struct DiffObjsResult {
|
||||
}
|
||||
|
||||
pub fn diff_objs(
|
||||
config: &DiffObjConfig,
|
||||
diff_config: &DiffObjConfig,
|
||||
mapping_config: &MappingConfig,
|
||||
left: Option<&ObjInfo>,
|
||||
right: Option<&ObjInfo>,
|
||||
prev: Option<&ObjInfo>,
|
||||
) -> Result<DiffObjsResult> {
|
||||
let symbol_matches = matching_symbols(left, right, prev, &config.symbol_mappings)?;
|
||||
let symbol_matches = matching_symbols(left, right, prev, mapping_config)?;
|
||||
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)));
|
||||
@@ -408,27 +229,34 @@ 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, config)?;
|
||||
let right_code = process_code_symbol(right_obj, right_symbol_ref, config)?;
|
||||
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_diff, right_diff) = diff_code(
|
||||
left_obj,
|
||||
right_obj,
|
||||
&left_code,
|
||||
&right_code,
|
||||
left_symbol_ref,
|
||||
right_symbol_ref,
|
||||
config,
|
||||
diff_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, config)?;
|
||||
let prev_code =
|
||||
process_code_symbol(prev_obj, prev_symbol_ref, diff_config)?;
|
||||
let (_, prev_diff) = diff_code(
|
||||
left_obj,
|
||||
right_obj,
|
||||
&right_code,
|
||||
&prev_code,
|
||||
right_symbol_ref,
|
||||
prev_symbol_ref,
|
||||
config,
|
||||
diff_config,
|
||||
)?;
|
||||
*prev_out.symbol_diff_mut(prev_symbol_ref) = prev_diff;
|
||||
}
|
||||
@@ -459,7 +287,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, config)?;
|
||||
let code = process_code_symbol(left_obj, left_symbol_ref, diff_config)?;
|
||||
*left_out.symbol_diff_mut(left_symbol_ref) =
|
||||
no_diff_code(&code, left_symbol_ref)?;
|
||||
}
|
||||
@@ -473,7 +301,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, config)?;
|
||||
let code = process_code_symbol(right_obj, right_symbol_ref, diff_config)?;
|
||||
*right_out.symbol_diff_mut(right_symbol_ref) =
|
||||
no_diff_code(&code, right_symbol_ref)?;
|
||||
}
|
||||
@@ -544,11 +372,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) = &config.symbol_mappings.selecting_left {
|
||||
generate_mapping_symbols(right_obj, right_name, left_obj, left_out, config)?;
|
||||
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(left_name) = &config.symbol_mappings.selecting_right {
|
||||
generate_mapping_symbols(left_obj, left_name, right_obj, right_out, config)?;
|
||||
if let Some(left_name) = &mapping_config.selecting_right {
|
||||
generate_mapping_symbols(left_obj, left_name, right_obj, right_out, diff_config)?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -572,7 +400,7 @@ fn generate_mapping_symbols(
|
||||
let Some(base_symbol_ref) = symbol_ref_by_name(base_obj, base_name) else {
|
||||
return Ok(());
|
||||
};
|
||||
let (base_section, base_symbol) = base_obj.section_symbol(base_symbol_ref);
|
||||
let (base_section, _base_symbol) = base_obj.section_symbol(base_symbol_ref);
|
||||
let Some(base_section) = base_section else {
|
||||
return Ok(());
|
||||
};
|
||||
@@ -583,15 +411,15 @@ fn generate_mapping_symbols(
|
||||
for (target_section_index, target_section) in
|
||||
target_obj.sections.iter().enumerate().filter(|(_, s)| s.kind == base_section.kind)
|
||||
{
|
||||
for (target_symbol_index, _target_symbol) in
|
||||
target_section.symbols.iter().enumerate().filter(|(_, s)| s.kind == base_symbol.kind)
|
||||
{
|
||||
for (target_symbol_index, _target_symbol) in target_section.symbols.iter().enumerate() {
|
||||
let target_symbol_ref =
|
||||
SymbolRef { section_idx: target_section_index, symbol_idx: target_symbol_index };
|
||||
match base_section.kind {
|
||||
ObjSectionKind::Code => {
|
||||
let target_code = process_code_symbol(target_obj, target_symbol_ref, config)?;
|
||||
let (left_diff, _right_diff) = diff_code(
|
||||
target_obj,
|
||||
base_obj,
|
||||
&target_code,
|
||||
base_code.as_ref().unwrap(),
|
||||
target_symbol_ref,
|
||||
@@ -631,7 +459,9 @@ struct SectionMatch {
|
||||
section_kind: ObjSectionKind,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, serde::Deserialize, serde::Serialize)]
|
||||
#[derive(Debug, Clone, Default, serde::Deserialize, serde::Serialize)]
|
||||
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify), tsify(from_wasm_abi))]
|
||||
#[serde(default)]
|
||||
pub struct MappingConfig {
|
||||
/// Manual symbol mappings
|
||||
pub mappings: SymbolMappings,
|
||||
@@ -751,7 +581,7 @@ fn matching_symbols(
|
||||
}
|
||||
}
|
||||
for (symbol_idx, symbol) in left.common.iter().enumerate() {
|
||||
let symbol_ref = SymbolRef { section_idx: left.sections.len(), symbol_idx };
|
||||
let symbol_ref = SymbolRef { section_idx: SECTION_COMMON, symbol_idx };
|
||||
if left_used.contains(&symbol_ref) {
|
||||
continue;
|
||||
}
|
||||
@@ -783,7 +613,7 @@ fn matching_symbols(
|
||||
}
|
||||
}
|
||||
for (symbol_idx, symbol) in right.common.iter().enumerate() {
|
||||
let symbol_ref = SymbolRef { section_idx: right.sections.len(), symbol_idx };
|
||||
let symbol_ref = SymbolRef { section_idx: SECTION_COMMON, symbol_idx };
|
||||
if right_used.contains(&symbol_ref) {
|
||||
continue;
|
||||
}
|
||||
@@ -876,7 +706,7 @@ fn find_common_symbol(obj: Option<&ObjInfo>, in_symbol: &ObjSymbol) -> Option<Sy
|
||||
let obj = obj?;
|
||||
for (symbol_idx, symbol) in obj.common.iter().enumerate() {
|
||||
if symbol.name == in_symbol.name {
|
||||
return Some(SymbolRef { section_idx: obj.sections.len(), symbol_idx });
|
||||
return Some(SymbolRef { section_idx: SECTION_COMMON, symbol_idx });
|
||||
}
|
||||
}
|
||||
None
|
||||
|
||||
50
objdiff-core/src/jobs/check_update.rs
Normal file
50
objdiff-core/src/jobs/check_update.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
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)))
|
||||
})
|
||||
}
|
||||
@@ -1,14 +1,10 @@
|
||||
use std::{fs, path::PathBuf, sync::mpsc::Receiver};
|
||||
use std::{fs, path::PathBuf, sync::mpsc::Receiver, task::Waker};
|
||||
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use const_format::formatcp;
|
||||
|
||||
use crate::{
|
||||
app::AppConfig,
|
||||
jobs::{
|
||||
objdiff::{run_make, BuildConfig, BuildStatus},
|
||||
start_job, update_status, Job, JobContext, JobResult, JobState,
|
||||
},
|
||||
build::{run_make, BuildConfig, BuildStatus},
|
||||
jobs::{start_job, update_status, Job, JobContext, JobResult, JobState},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -23,37 +19,7 @@ pub struct CreateScratchConfig {
|
||||
pub compiler_flags: String,
|
||||
pub function_name: String,
|
||||
pub target_obj: PathBuf,
|
||||
}
|
||||
|
||||
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(),
|
||||
})
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
pub preset_id: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
@@ -97,22 +63,25 @@ 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).unwrap();
|
||||
let diff_flags = serde_json::to_string(&diff_flags)?;
|
||||
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()))?;
|
||||
let form = reqwest::blocking::multipart::Form::new()
|
||||
let mut form = reqwest::blocking::multipart::Form::new()
|
||||
.text("compiler", config.compiler.clone())
|
||||
.text("platform", config.platform.clone())
|
||||
.text("compiler_flags", config.compiler_flags.clone())
|
||||
.text("diff_label", config.function_name.clone())
|
||||
.text("diff_flags", diff_flags)
|
||||
.text("context", context.unwrap_or_default())
|
||||
.text("source_code", "// Move related code from Context tab to here")
|
||||
.part("target_obj", file);
|
||||
.text("source_code", "// Move related code from Context tab to here");
|
||||
if let Some(preset) = config.preset_id {
|
||||
form = form.text("preset", preset.to_string());
|
||||
}
|
||||
form = form.part("target_obj", file);
|
||||
let client = reqwest::blocking::Client::new();
|
||||
let response = client
|
||||
.post(formatcp!("{API_HOST}/api/scratch"))
|
||||
.post(format!("{API_HOST}/api/scratch"))
|
||||
.multipart(form)
|
||||
.send()
|
||||
.map_err(|e| anyhow!("Failed to send request: {}", e))?;
|
||||
@@ -126,8 +95,8 @@ fn run_create_scratch(
|
||||
Ok(Box::from(CreateScratchResult { scratch_url }))
|
||||
}
|
||||
|
||||
pub fn start_create_scratch(ctx: &egui::Context, config: CreateScratchConfig) -> JobState {
|
||||
start_job(ctx, "Create scratch", Job::CreateScratch, move |context, cancel| {
|
||||
pub fn start_create_scratch(waker: Waker, config: CreateScratchConfig) -> JobState {
|
||||
start_job(waker, "Create scratch", Job::CreateScratch, move |context, cancel| {
|
||||
run_create_scratch(&context, cancel, config)
|
||||
.map(|result| JobResult::CreateScratch(Some(result)))
|
||||
})
|
||||
@@ -4,6 +4,7 @@ use std::{
|
||||
mpsc::{Receiver, Sender, TryRecvError},
|
||||
Arc, RwLock,
|
||||
},
|
||||
task::Waker,
|
||||
thread::JoinHandle,
|
||||
};
|
||||
|
||||
@@ -53,7 +54,6 @@ 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,12 +96,53 @@ 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 egui: egui::Context,
|
||||
pub waker: Waker,
|
||||
}
|
||||
|
||||
pub struct JobState {
|
||||
@@ -137,7 +178,7 @@ fn should_cancel(rx: &Receiver<()>) -> bool {
|
||||
}
|
||||
|
||||
fn start_job(
|
||||
ctx: &egui::Context,
|
||||
waker: Waker,
|
||||
title: &str,
|
||||
kind: Job,
|
||||
run: impl FnOnce(JobContext, Receiver<()>) -> Result<JobResult> + Send + 'static,
|
||||
@@ -149,22 +190,20 @@ fn start_job(
|
||||
status: String::new(),
|
||||
error: None,
|
||||
}));
|
||||
let context = JobContext { status: status.clone(), egui: ctx.clone() };
|
||||
let context_inner = JobContext { status: status.clone(), egui: ctx.clone() };
|
||||
let context = JobContext { status: status.clone(), waker: waker.clone() };
|
||||
let context_inner = JobContext { status: status.clone(), waker };
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
let handle = std::thread::spawn(move || {
|
||||
return match run(context_inner, rx) {
|
||||
Ok(state) => state,
|
||||
Err(e) => {
|
||||
if let Ok(mut w) = status.write() {
|
||||
w.error = Some(e);
|
||||
}
|
||||
JobResult::None
|
||||
let handle = std::thread::spawn(move || match run(context_inner, rx) {
|
||||
Ok(state) => state,
|
||||
Err(e) => {
|
||||
if let Ok(mut w) = status.write() {
|
||||
w.error = Some(e);
|
||||
}
|
||||
};
|
||||
JobResult::None
|
||||
}
|
||||
});
|
||||
let id = JOB_ID.fetch_add(1, Ordering::Relaxed);
|
||||
log::info!("Started job {}", id);
|
||||
// log::info!("Started job {}", id); TODO
|
||||
JobState { id, kind, handle: Some(handle), context, cancel: tx }
|
||||
}
|
||||
|
||||
@@ -186,6 +225,6 @@ fn update_status(
|
||||
w.status = str;
|
||||
}
|
||||
drop(w);
|
||||
context.egui.request_repaint();
|
||||
context.waker.wake_by_ref();
|
||||
Ok(())
|
||||
}
|
||||
195
objdiff-core/src/jobs/objdiff.rs
Normal file
195
objdiff-core/src/jobs/objdiff.rs
Normal file
@@ -0,0 +1,195 @@
|
||||
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)))
|
||||
})
|
||||
}
|
||||
@@ -3,14 +3,19 @@ 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},
|
||||
update::build_updater,
|
||||
};
|
||||
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,
|
||||
}
|
||||
|
||||
pub struct UpdateResult {
|
||||
pub exe_path: PathBuf,
|
||||
@@ -19,16 +24,15 @@ pub struct UpdateResult {
|
||||
fn run_update(
|
||||
status: &JobContext,
|
||||
cancel: Receiver<()>,
|
||||
bin_name: String,
|
||||
config: UpdateConfig,
|
||||
) -> Result<Box<UpdateResult>> {
|
||||
update_status(status, "Fetching latest release".to_string(), 0, 3, &cancel)?;
|
||||
let updater = build_updater().context("Failed to create release updater")?;
|
||||
let updater = (config.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 == bin_name)
|
||||
.ok_or_else(|| anyhow::Error::msg(format!("No release asset for {bin_name}")))?;
|
||||
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))
|
||||
})?;
|
||||
|
||||
update_status(status, "Downloading release".to_string(), 1, 3, &cancel)?;
|
||||
let tmp_dir = tempfile::Builder::new().prefix("update").tempdir_in(current_dir()?)?;
|
||||
@@ -47,9 +51,7 @@ fn run_update(
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::{fs, os::unix::fs::PermissionsExt};
|
||||
let mut perms = fs::metadata(&target_file)?.permissions();
|
||||
perms.set_mode(0o755);
|
||||
fs::set_permissions(&target_file, perms)?;
|
||||
fs::set_permissions(&target_file, fs::Permissions::from_mode(0o755))?;
|
||||
}
|
||||
tmp_dir.close()?;
|
||||
|
||||
@@ -57,8 +59,8 @@ fn run_update(
|
||||
Ok(Box::from(UpdateResult { exe_path: target_file }))
|
||||
}
|
||||
|
||||
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)
|
||||
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)
|
||||
})
|
||||
}
|
||||
@@ -2,10 +2,15 @@
|
||||
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")]
|
||||
pub mod util;
|
||||
|
||||
@@ -85,6 +85,9 @@ pub enum ObjInsArg {
|
||||
}
|
||||
|
||||
impl ObjInsArg {
|
||||
#[inline]
|
||||
pub fn is_plain_text(&self) -> bool { matches!(self, ObjInsArg::PlainText(_)) }
|
||||
|
||||
pub fn loose_eq(&self, other: &ObjInsArg) -> bool {
|
||||
match (self, other) {
|
||||
(ObjInsArg::Arg(a), ObjInsArg::Arg(b)) => a.loose_eq(b),
|
||||
@@ -100,7 +103,7 @@ pub struct ObjIns {
|
||||
pub address: u64,
|
||||
pub size: u8,
|
||||
pub op: u16,
|
||||
pub mnemonic: String,
|
||||
pub mnemonic: Cow<'static, str>,
|
||||
pub args: Vec<ObjInsArg>,
|
||||
pub reloc: Option<ObjReloc>,
|
||||
pub branch_dest: Option<u64>,
|
||||
@@ -112,6 +115,14 @@ pub struct ObjIns {
|
||||
pub orig: Option<String>,
|
||||
}
|
||||
|
||||
impl ObjIns {
|
||||
/// Iterate over non-PlainText arguments.
|
||||
#[inline]
|
||||
pub fn iter_args(&self) -> impl DoubleEndedIterator<Item = &ObjInsArg> {
|
||||
self.args.iter().filter(|a| !a.is_plain_text())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
|
||||
pub enum ObjSymbolKind {
|
||||
#[default]
|
||||
@@ -131,7 +142,7 @@ pub struct ObjSymbol {
|
||||
pub size_known: bool,
|
||||
pub kind: ObjSymbolKind,
|
||||
pub flags: ObjSymbolFlagSet,
|
||||
pub addend: i64,
|
||||
pub orig_section_index: Option<usize>,
|
||||
/// Original virtual address (from .note.split section)
|
||||
pub virtual_address: Option<u64>,
|
||||
/// Original index in object symbol table
|
||||
@@ -155,7 +166,7 @@ pub struct ObjReloc {
|
||||
pub flags: RelocationFlags,
|
||||
pub address: u64,
|
||||
pub target: ObjSymbol,
|
||||
pub target_section: Option<String>,
|
||||
pub addend: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
|
||||
@@ -164,9 +175,11 @@ pub struct SymbolRef {
|
||||
pub symbol_idx: usize,
|
||||
}
|
||||
|
||||
pub const SECTION_COMMON: usize = usize::MAX - 1;
|
||||
|
||||
impl ObjInfo {
|
||||
pub fn section_symbol(&self, symbol_ref: SymbolRef) -> (Option<&ObjSection>, &ObjSymbol) {
|
||||
if symbol_ref.section_idx == self.sections.len() {
|
||||
if symbol_ref.section_idx == SECTION_COMMON {
|
||||
let symbol = &self.common[symbol_ref.symbol_idx];
|
||||
return (None, symbol);
|
||||
}
|
||||
|
||||
@@ -13,8 +13,8 @@ use object::{
|
||||
endian::LittleEndian as LE,
|
||||
pe::{ImageAuxSymbolFunctionBeginEnd, ImageLinenumber},
|
||||
read::coff::{CoffFile, CoffHeader, ImageSymbol},
|
||||
BinaryFormat, File, Object, ObjectSection, ObjectSymbol, RelocationTarget, SectionIndex,
|
||||
SectionKind, Symbol, SymbolIndex, SymbolKind, SymbolScope, SymbolSection,
|
||||
BinaryFormat, File, Object, ObjectSection, ObjectSymbol, RelocationTarget, Section,
|
||||
SectionIndex, SectionKind, Symbol, SymbolIndex, SymbolKind, SymbolScope,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
@@ -41,7 +41,6 @@ fn to_obj_symbol(
|
||||
arch: &dyn ObjArch,
|
||||
obj_file: &File<'_>,
|
||||
symbol: &Symbol<'_, '_>,
|
||||
addend: i64,
|
||||
split_meta: Option<&SplitMeta>,
|
||||
) -> Result<ObjSymbol> {
|
||||
let mut name = symbol.name().context("Failed to process symbol name")?;
|
||||
@@ -65,10 +64,8 @@ fn to_obj_symbol(
|
||||
if obj_file.format() == BinaryFormat::Elf && symbol.scope() == SymbolScope::Linkage {
|
||||
flags = ObjSymbolFlagSet(flags.0 | ObjSymbolFlags::Hidden);
|
||||
}
|
||||
if arch
|
||||
.ppc()
|
||||
.and_then(|a| a.extab.as_ref())
|
||||
.map_or(false, |e| e.contains_key(&symbol.index().0))
|
||||
#[cfg(feature = "ppc")]
|
||||
if arch.ppc().and_then(|a| a.extab.as_ref()).is_some_and(|e| e.contains_key(&symbol.index().0))
|
||||
{
|
||||
flags = ObjSymbolFlagSet(flags.0 | ObjSymbolFlags::HasExtra);
|
||||
}
|
||||
@@ -111,7 +108,7 @@ fn to_obj_symbol(
|
||||
size_known: symbol.size() != 0,
|
||||
kind,
|
||||
flags,
|
||||
addend,
|
||||
orig_section_index: symbol.section_index().map(|i| i.0),
|
||||
virtual_address,
|
||||
original_index: Some(symbol.index().0),
|
||||
bytes: bytes.to_vec(),
|
||||
@@ -177,7 +174,7 @@ fn symbols_by_section(
|
||||
continue;
|
||||
}
|
||||
}
|
||||
result.push(to_obj_symbol(arch, obj_file, symbol, 0, split_meta)?);
|
||||
result.push(to_obj_symbol(arch, obj_file, symbol, split_meta)?);
|
||||
}
|
||||
result.sort_by(|a, b| a.address.cmp(&b.address).then(a.size.cmp(&b.size)));
|
||||
let mut iter = result.iter_mut().peekable();
|
||||
@@ -217,7 +214,7 @@ fn symbols_by_section(
|
||||
ObjSectionKind::Data | ObjSectionKind::Bss => ObjSymbolKind::Object,
|
||||
},
|
||||
flags: Default::default(),
|
||||
addend: 0,
|
||||
orig_section_index: Some(section.orig_index),
|
||||
virtual_address: None,
|
||||
original_index: None,
|
||||
bytes: Vec::new(),
|
||||
@@ -234,7 +231,7 @@ fn common_symbols(
|
||||
obj_file
|
||||
.symbols()
|
||||
.filter(Symbol::is_common)
|
||||
.map(|symbol| to_obj_symbol(arch, obj_file, &symbol, 0, split_meta))
|
||||
.map(|symbol| to_obj_symbol(arch, obj_file, &symbol, split_meta))
|
||||
.collect::<Result<Vec<ObjSymbol>>>()
|
||||
}
|
||||
|
||||
@@ -245,10 +242,18 @@ fn best_symbol<'r, 'data, 'file>(
|
||||
symbols: &'r [Symbol<'data, 'file>],
|
||||
address: u64,
|
||||
) -> Option<&'r Symbol<'data, 'file>> {
|
||||
let closest_symbol_index = match symbols.binary_search_by_key(&address, |s| s.address()) {
|
||||
let mut closest_symbol_index = match symbols.binary_search_by_key(&address, |s| s.address()) {
|
||||
Ok(index) => Some(index),
|
||||
Err(index) => index.checked_sub(1),
|
||||
}?;
|
||||
// The binary search may not find the first symbol at the address, so work backwards
|
||||
let target_address = symbols[closest_symbol_index].address();
|
||||
while let Some(prev_index) = closest_symbol_index.checked_sub(1) {
|
||||
if symbols[prev_index].address() != target_address {
|
||||
break;
|
||||
}
|
||||
closest_symbol_index = prev_index;
|
||||
}
|
||||
let mut best_symbol: Option<&'r Symbol<'data, 'file>> = None;
|
||||
for symbol in symbols.iter().skip(closest_symbol_index) {
|
||||
if symbol.address() > address {
|
||||
@@ -276,24 +281,15 @@ fn best_symbol<'r, 'data, 'file>(
|
||||
fn find_section_symbol(
|
||||
arch: &dyn ObjArch,
|
||||
obj_file: &File<'_>,
|
||||
section: &Section,
|
||||
section_symbols: &[Symbol<'_, '_>],
|
||||
target: &Symbol<'_, '_>,
|
||||
address: u64,
|
||||
split_meta: Option<&SplitMeta>,
|
||||
) -> Result<ObjSymbol> {
|
||||
if let Some(symbol) = best_symbol(section_symbols, address) {
|
||||
return to_obj_symbol(
|
||||
arch,
|
||||
obj_file,
|
||||
symbol,
|
||||
address as i64 - symbol.address() as i64,
|
||||
split_meta,
|
||||
);
|
||||
return to_obj_symbol(arch, obj_file, symbol, split_meta);
|
||||
}
|
||||
// Fallback to section symbol
|
||||
let section_index =
|
||||
target.section_index().ok_or_else(|| anyhow::Error::msg("Unknown section index"))?;
|
||||
let section = obj_file.section_by_index(section_index)?;
|
||||
Ok(ObjSymbol {
|
||||
name: section.name()?.to_string(),
|
||||
demangled_name: None,
|
||||
@@ -303,7 +299,7 @@ fn find_section_symbol(
|
||||
size_known: false,
|
||||
kind: ObjSymbolKind::Section,
|
||||
flags: Default::default(),
|
||||
addend: address as i64 - section.address() as i64,
|
||||
orig_section_index: Some(section.index().0),
|
||||
virtual_address: None,
|
||||
original_index: None,
|
||||
bytes: Vec::new(),
|
||||
@@ -314,7 +310,7 @@ fn relocations_by_section(
|
||||
arch: &dyn ObjArch,
|
||||
obj_file: &File<'_>,
|
||||
section: &ObjSection,
|
||||
section_symbols: &[Symbol<'_, '_>],
|
||||
section_symbols: &[Vec<Symbol<'_, '_>>],
|
||||
split_meta: Option<&SplitMeta>,
|
||||
) -> Result<Vec<ObjReloc>> {
|
||||
let obj_section = obj_file.section_by_index(SectionIndex(section.orig_index))?;
|
||||
@@ -336,40 +332,43 @@ fn relocations_by_section(
|
||||
};
|
||||
symbol
|
||||
}
|
||||
RelocationTarget::Absolute => {
|
||||
log::warn!("Ignoring absolute relocation @ {}:{:#x}", section.name, address);
|
||||
continue;
|
||||
}
|
||||
_ => bail!("Unhandled relocation target: {:?}", reloc.target()),
|
||||
};
|
||||
let flags = reloc.flags(); // TODO validate reloc here?
|
||||
let target_section = match symbol.section() {
|
||||
SymbolSection::Common => Some(".comm".to_string()),
|
||||
SymbolSection::Section(idx) => {
|
||||
obj_file.section_by_index(idx).and_then(|s| s.name().map(|s| s.to_string())).ok()
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let addend = if reloc.has_implicit_addend() {
|
||||
let mut addend = if reloc.has_implicit_addend() {
|
||||
arch.implcit_addend(obj_file, section, address, &reloc)?
|
||||
} else {
|
||||
reloc.addend()
|
||||
};
|
||||
// println!("Reloc: {reloc:?}, symbol: {symbol:?}, addend: {addend:#x}");
|
||||
let target = match symbol.kind() {
|
||||
SymbolKind::Text | SymbolKind::Data | SymbolKind::Label | SymbolKind::Unknown => {
|
||||
to_obj_symbol(arch, obj_file, &symbol, addend, split_meta)
|
||||
to_obj_symbol(arch, obj_file, &symbol, split_meta)?
|
||||
}
|
||||
SymbolKind::Section => {
|
||||
ensure!(addend >= 0, "Negative addend in reloc: {addend}");
|
||||
find_section_symbol(
|
||||
ensure!(addend >= 0, "Negative addend in section reloc: {addend}");
|
||||
let section_index = symbol
|
||||
.section_index()
|
||||
.ok_or_else(|| anyhow!("Section symbol {symbol:?} has no section index"))?;
|
||||
let section = obj_file.section_by_index(section_index)?;
|
||||
let symbol = find_section_symbol(
|
||||
arch,
|
||||
obj_file,
|
||||
section_symbols,
|
||||
&symbol,
|
||||
§ion,
|
||||
§ion_symbols[section_index.0],
|
||||
addend as u64,
|
||||
split_meta,
|
||||
)
|
||||
)?;
|
||||
// Adjust addend to be relative to the selected symbol
|
||||
addend = (symbol.address - section.address()) as i64;
|
||||
symbol
|
||||
}
|
||||
kind => Err(anyhow!("Unhandled relocation symbol type {kind:?}")),
|
||||
}?;
|
||||
relocations.push(ObjReloc { flags, address, target, target_section });
|
||||
kind => bail!("Unhandled relocation symbol type {kind:?}"),
|
||||
};
|
||||
relocations.push(ObjReloc { flags, address, target, addend });
|
||||
}
|
||||
Ok(relocations)
|
||||
}
|
||||
@@ -434,9 +433,9 @@ fn line_info(obj_file: &File<'_>, sections: &mut [ObjSection], obj_data: &[u8])
|
||||
let mut text_sections =
|
||||
obj_file.sections().filter(|s| s.kind() == SectionKind::Text);
|
||||
let section_index = text_sections.next().map(|s| s.index().0);
|
||||
let mut lines = section_index.map(|index| {
|
||||
&mut sections.iter_mut().find(|s| s.orig_index == index).unwrap().line_info
|
||||
});
|
||||
let mut lines = section_index
|
||||
.and_then(|index| sections.iter_mut().find(|s| s.orig_index == index))
|
||||
.map(|s| &mut s.line_info);
|
||||
|
||||
let mut rows = program.rows();
|
||||
while let Some((_header, row)) = rows.next_row()? {
|
||||
@@ -447,13 +446,9 @@ fn line_info(obj_file: &File<'_>, sections: &mut [ObjSection], obj_data: &[u8])
|
||||
// The next row is the start of a new sequence, which means we must
|
||||
// advance to the next .text section.
|
||||
let section_index = text_sections.next().map(|s| s.index().0);
|
||||
lines = section_index.map(|index| {
|
||||
&mut sections
|
||||
.iter_mut()
|
||||
.find(|s| s.orig_index == index)
|
||||
.unwrap()
|
||||
.line_info
|
||||
});
|
||||
lines = section_index
|
||||
.and_then(|index| sections.iter_mut().find(|s| s.orig_index == index))
|
||||
.map(|s| &mut s.line_info);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -591,7 +586,7 @@ fn update_combined_symbol(symbol: ObjSymbol, address_change: i64) -> Result<ObjS
|
||||
size_known: symbol.size_known,
|
||||
kind: symbol.kind,
|
||||
flags: symbol.flags,
|
||||
addend: symbol.addend,
|
||||
orig_section_index: symbol.orig_section_index,
|
||||
virtual_address: if let Some(virtual_address) = symbol.virtual_address {
|
||||
Some((virtual_address as i64 + address_change).try_into()?)
|
||||
} else {
|
||||
@@ -617,8 +612,8 @@ fn combine_sections(section: ObjSection, combine: ObjSection) -> Result<ObjSecti
|
||||
relocations.push(ObjReloc {
|
||||
flags: reloc.flags,
|
||||
address: (reloc.address as i64 + address_change).try_into()?,
|
||||
target: reloc.target, // TODO: Should be updated?
|
||||
target_section: reloc.target_section, // TODO: Same as above
|
||||
target: reloc.target, // TODO: Should be updated?
|
||||
addend: reloc.addend,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -698,27 +693,38 @@ pub fn parse(data: &[u8], config: &DiffObjConfig) -> Result<ObjInfo> {
|
||||
let obj_file = File::parse(data)?;
|
||||
let arch = new_arch(&obj_file)?;
|
||||
let split_meta = split_meta(&obj_file)?;
|
||||
let mut sections = filter_sections(&obj_file, split_meta.as_ref())?;
|
||||
let mut name_counts: HashMap<String, u32> = HashMap::new();
|
||||
for section in &mut sections {
|
||||
|
||||
// Create sorted symbol list for each section
|
||||
let mut section_symbols = Vec::with_capacity(obj_file.sections().count());
|
||||
for section in obj_file.sections() {
|
||||
let mut symbols = obj_file
|
||||
.symbols()
|
||||
.filter(|s| s.section_index() == Some(SectionIndex(section.orig_index)))
|
||||
.filter(|s| s.section_index() == Some(section.index()))
|
||||
.collect::<Vec<_>>();
|
||||
symbols.sort_by_key(|s| s.address());
|
||||
let section_index = section.index().0;
|
||||
if section_index >= section_symbols.len() {
|
||||
section_symbols.resize_with(section_index + 1, Vec::new);
|
||||
}
|
||||
section_symbols[section_index] = symbols;
|
||||
}
|
||||
|
||||
let mut sections = filter_sections(&obj_file, split_meta.as_ref())?;
|
||||
let mut section_name_counts: HashMap<String, u32> = HashMap::new();
|
||||
for section in &mut sections {
|
||||
section.symbols = symbols_by_section(
|
||||
arch.as_ref(),
|
||||
&obj_file,
|
||||
section,
|
||||
&symbols,
|
||||
§ion_symbols[section.orig_index],
|
||||
split_meta.as_ref(),
|
||||
&mut name_counts,
|
||||
&mut section_name_counts,
|
||||
)?;
|
||||
section.relocations = relocations_by_section(
|
||||
arch.as_ref(),
|
||||
&obj_file,
|
||||
section,
|
||||
&symbols,
|
||||
§ion_symbols,
|
||||
split_meta.as_ref(),
|
||||
)?;
|
||||
}
|
||||
|
||||
@@ -25,24 +25,23 @@ wsl = []
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
bytes = "1.7"
|
||||
bytes = "1.9"
|
||||
cfg-if = "1.0"
|
||||
const_format = "0.2"
|
||||
cwdemangle = "1.0"
|
||||
cwextab = "1.0.2"
|
||||
cwextab = "1.0"
|
||||
dirs = "5.0"
|
||||
egui = "0.29"
|
||||
egui_extras = "0.29"
|
||||
egui = "0.30"
|
||||
egui_extras = "0.30"
|
||||
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"
|
||||
pollster = "0.3"
|
||||
pollster = "0.4"
|
||||
regex = "1.11"
|
||||
rfd = { version = "0.15" } #, default-features = false, features = ['xdg-portal']
|
||||
rlwinmdec = "1.0"
|
||||
@@ -51,12 +50,11 @@ serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
shell-escape = "0.1"
|
||||
strum = { version = "0.26", features = ["derive"] }
|
||||
tempfile = "3.13"
|
||||
time = { version = "0.3", features = ["formatting", "local-offset"] }
|
||||
|
||||
# Keep version in sync with egui
|
||||
[dependencies.eframe]
|
||||
version = "0.29"
|
||||
version = "0.30"
|
||||
features = [
|
||||
"default_fonts",
|
||||
"persistence",
|
||||
@@ -67,7 +65,7 @@ default-features = false
|
||||
|
||||
# Keep version in sync with eframe
|
||||
[dependencies.wgpu]
|
||||
version = "22.1"
|
||||
version = "23.0"
|
||||
features = [
|
||||
"dx12",
|
||||
"metal",
|
||||
@@ -76,18 +74,7 @@ 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]
|
||||
@@ -95,7 +82,7 @@ exec = "0.3"
|
||||
|
||||
# native:
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
tracing-subscriber = "0.3"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
|
||||
# web:
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
|
||||
@@ -11,28 +11,27 @@ use std::{
|
||||
};
|
||||
|
||||
use filetime::FileTime;
|
||||
use globset::{Glob, GlobSet};
|
||||
use notify::{RecursiveMode, Watcher};
|
||||
use globset::Glob;
|
||||
use objdiff_core::{
|
||||
build::watcher::{create_watcher, Watcher},
|
||||
config::{
|
||||
build_globset, save_project_config, ProjectConfig, ProjectConfigInfo, ProjectObject,
|
||||
ScratchConfig, SymbolMappings, DEFAULT_WATCH_PATTERNS,
|
||||
build_globset, default_watch_patterns, 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::{
|
||||
objdiff::{start_build, ObjDiffConfig},
|
||||
Job, JobQueue, JobResult, JobStatus,
|
||||
},
|
||||
jobs::{create_objdiff_config, egui_waker, start_build},
|
||||
views::{
|
||||
appearance::{appearance_window, Appearance},
|
||||
config::{
|
||||
arch_config_window, config_ui, project_window, ConfigViewState, CONFIG_DISABLED_TEXT,
|
||||
arch_config_window, config_ui, general_config_ui, project_window, ConfigViewState,
|
||||
CONFIG_DISABLED_TEXT,
|
||||
},
|
||||
data_diff::data_diff_ui,
|
||||
debug::debug_window,
|
||||
@@ -121,11 +120,6 @@ 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>,
|
||||
@@ -300,7 +294,7 @@ impl AppState {
|
||||
let Some(object) = self.config.selected_obj.as_mut() else {
|
||||
return;
|
||||
};
|
||||
object.symbol_mappings.remove_by_right(right);
|
||||
object.symbol_mappings.retain(|_, r| r != right);
|
||||
self.selecting_left = Some(right.to_string());
|
||||
self.queue_reload = true;
|
||||
self.save_config();
|
||||
@@ -310,7 +304,7 @@ impl AppState {
|
||||
let Some(object) = self.config.selected_obj.as_mut() else {
|
||||
return;
|
||||
};
|
||||
object.symbol_mappings.remove_by_left(left);
|
||||
object.symbol_mappings.retain(|l, _| l != left);
|
||||
self.selecting_right = Some(left.to_string());
|
||||
self.queue_reload = true;
|
||||
self.save_config();
|
||||
@@ -323,10 +317,8 @@ impl AppState {
|
||||
};
|
||||
self.selecting_left = None;
|
||||
self.selecting_right = None;
|
||||
if left == right {
|
||||
object.symbol_mappings.remove_by_left(&left);
|
||||
object.symbol_mappings.remove_by_right(&right);
|
||||
} else {
|
||||
object.symbol_mappings.retain(|l, r| l != &left && r != &right);
|
||||
if left != right {
|
||||
object.symbol_mappings.insert(left.clone(), right.clone());
|
||||
}
|
||||
self.queue_reload = true;
|
||||
@@ -399,7 +391,7 @@ pub struct App {
|
||||
view_state: ViewState,
|
||||
state: AppStateRef,
|
||||
modified: Arc<AtomicBool>,
|
||||
watcher: Option<notify::RecommendedWatcher>,
|
||||
watcher: Option<Watcher>,
|
||||
app_path: Option<PathBuf>,
|
||||
relaunch_path: Rc<Mutex<Option<PathBuf>>>,
|
||||
should_relaunch: bool,
|
||||
@@ -474,59 +466,27 @@ impl App {
|
||||
|
||||
let ViewState { jobs, diff_state, config_state, .. } = &mut self.view_state;
|
||||
|
||||
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),
|
||||
}));
|
||||
}
|
||||
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;
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
jobs.results.append(&mut results);
|
||||
jobs.clear_finished();
|
||||
|
||||
_ => true,
|
||||
});
|
||||
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;
|
||||
@@ -572,7 +532,7 @@ impl App {
|
||||
match build_globset(&state.config.watch_patterns)
|
||||
.map_err(anyhow::Error::new)
|
||||
.and_then(|globset| {
|
||||
create_watcher(ctx.clone(), self.modified.clone(), project_dir, globset)
|
||||
create_watcher(self.modified.clone(), project_dir, globset, egui_waker(ctx))
|
||||
.map_err(anyhow::Error::new)
|
||||
}) {
|
||||
Ok(watcher) => self.watcher = Some(watcher),
|
||||
@@ -619,15 +579,15 @@ impl App {
|
||||
&& state.config.selected_obj.is_some()
|
||||
&& !jobs.is_running(Job::ObjDiff)
|
||||
{
|
||||
jobs.push(start_build(ctx, ObjDiffConfig::from_state(state)));
|
||||
start_build(ctx, jobs, create_objdiff_config(state));
|
||||
state.queue_build = false;
|
||||
state.queue_reload = false;
|
||||
} else if state.queue_reload && !jobs.is_running(Job::ObjDiff) {
|
||||
let mut diff_config = ObjDiffConfig::from_state(state);
|
||||
let mut diff_config = create_objdiff_config(state);
|
||||
// Don't build, just reload the current files
|
||||
diff_config.build_base = false;
|
||||
diff_config.build_target = false;
|
||||
jobs.push(start_build(ctx, diff_config));
|
||||
start_build(ctx, jobs, diff_config);
|
||||
state.queue_reload = false;
|
||||
}
|
||||
|
||||
@@ -767,37 +727,9 @@ impl eframe::App for App {
|
||||
&mut diff_state.symbol_state.show_hidden_symbols,
|
||||
"Show hidden symbols",
|
||||
);
|
||||
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;
|
||||
}
|
||||
ui.separator();
|
||||
general_config_ui(ui, &mut state);
|
||||
ui.separator();
|
||||
if ui.button("Clear custom symbol mappings").clicked() {
|
||||
state.clear_mappings();
|
||||
diff_state.post_build_nav = Some(DiffViewNavigation::symbol_diff());
|
||||
@@ -854,40 +786,6 @@ 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) {
|
||||
|
||||
@@ -71,6 +71,7 @@ impl ScratchConfigV1 {
|
||||
c_flags: self.c_flags,
|
||||
ctx_path: self.ctx_path,
|
||||
build_ctx: self.build_ctx.then_some(true),
|
||||
preset_id: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -159,7 +160,6 @@ 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,21 @@ pub enum ProjectObjectNode {
|
||||
Dir(String, Vec<ProjectObjectNode>),
|
||||
}
|
||||
|
||||
fn join_single_dir_entries(nodes: &mut Vec<ProjectObjectNode>) {
|
||||
for node in nodes {
|
||||
if let ProjectObjectNode::Dir(my_name, my_nodes) = node {
|
||||
join_single_dir_entries(my_nodes);
|
||||
// If this directory consists of a single sub-directory...
|
||||
if let [ProjectObjectNode::Dir(sub_name, sub_nodes)] = &mut my_nodes[..] {
|
||||
// ... join the two names with a path separator and eliminate the layer
|
||||
*my_name += "/";
|
||||
*my_name += sub_name;
|
||||
*my_nodes = std::mem::take(sub_nodes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn find_dir<'a>(
|
||||
name: &str,
|
||||
nodes: &'a mut Vec<ProjectObjectNode>,
|
||||
@@ -60,6 +75,14 @@ fn build_nodes(
|
||||
let filename = path.file_name().unwrap().to_str().unwrap().to_string();
|
||||
out_nodes.push(ProjectObjectNode::Unit(filename, idx));
|
||||
}
|
||||
// Within the top-level module directories, join paths. Leave the
|
||||
// top-level name intact though since it's the module name.
|
||||
for node in &mut nodes {
|
||||
if let ProjectObjectNode::Dir(_, sub_nodes) = node {
|
||||
join_single_dir_entries(sub_nodes);
|
||||
}
|
||||
}
|
||||
|
||||
nodes
|
||||
}
|
||||
|
||||
@@ -76,9 +99,15 @@ 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);
|
||||
state.config.watch_patterns = project_config.watch_patterns.clone().unwrap_or_else(|| {
|
||||
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect()
|
||||
});
|
||||
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.watcher_change = true;
|
||||
state.objects = project_config.units.clone().unwrap_or_default();
|
||||
state.object_nodes = build_nodes(
|
||||
|
||||
@@ -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(), default_font_data.font_data);
|
||||
fonts.font_data.insert(default_font_ref.full_name(), Arc::new(default_font_data.font_data));
|
||||
fonts
|
||||
.families
|
||||
.entry(egui::FontFamily::Name(Arc::from(family.family_name)))
|
||||
|
||||
108
objdiff-gui/src/hotkeys.rs
Normal file
108
objdiff-gui/src/hotkeys.rs
Normal file
@@ -0,0 +1,108 @@
|
||||
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))
|
||||
}
|
||||
141
objdiff-gui/src/jobs.rs
Normal file
141
objdiff-gui/src/jobs.rs
Normal file
@@ -0,0 +1,141 @@
|
||||
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 })
|
||||
});
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
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)))
|
||||
})
|
||||
}
|
||||
@@ -1,328 +0,0 @@
|
||||
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)))
|
||||
})
|
||||
}
|
||||
@@ -4,6 +4,7 @@ mod app;
|
||||
mod app_config;
|
||||
mod config;
|
||||
mod fonts;
|
||||
mod hotkeys;
|
||||
mod jobs;
|
||||
mod update;
|
||||
mod views;
|
||||
@@ -87,14 +88,29 @@ fn main() -> ExitCode {
|
||||
}
|
||||
#[cfg(feature = "wgpu")]
|
||||
{
|
||||
use eframe::egui_wgpu::wgpu::Backends;
|
||||
use eframe::egui_wgpu::{wgpu::Backends, WgpuSetup};
|
||||
if graphics_config.desired_backend.is_supported() {
|
||||
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,
|
||||
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!(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -111,6 +127,8 @@ 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;
|
||||
@@ -122,7 +140,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.supported_backends = Default::default();
|
||||
native_options.wgpu_options.wgpu_setup = WgpuConfiguration::default().wgpu_setup;
|
||||
if let Err(e) = run_eframe(
|
||||
native_options.clone(),
|
||||
utc_offset,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
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;
|
||||
@@ -26,8 +28,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() -> self_update::errors::Result<Box<dyn ReleaseUpdate>> {
|
||||
self_update::backends::github::Update::configure()
|
||||
pub fn build_updater() -> Result<Box<dyn ReleaseUpdate>> {
|
||||
Ok(self_update::backends::github::Update::configure()
|
||||
.repo_owner(GITHUB_USER)
|
||||
.repo_name(GITHUB_REPO)
|
||||
// bin_name is required, but unused?
|
||||
@@ -35,5 +37,5 @@ pub fn build_updater() -> self_update::errors::Result<Box<dyn ReleaseUpdate>> {
|
||||
.no_confirm(true)
|
||||
.show_output(false)
|
||||
.current_version(cargo_crate_version!())
|
||||
.build()
|
||||
.build()?)
|
||||
}
|
||||
|
||||
@@ -205,7 +205,7 @@ pub const DEFAULT_COLOR_ROTATION: [Color32; 9] = [
|
||||
Color32::from_rgb(255, 0, 0),
|
||||
Color32::from_rgb(255, 255, 0),
|
||||
Color32::from_rgb(255, 192, 203),
|
||||
Color32::from_rgb(0, 0, 255),
|
||||
Color32::from_rgb(128, 128, 255),
|
||||
Color32::from_rgb(0, 255, 0),
|
||||
Color32::from_rgb(213, 138, 138),
|
||||
];
|
||||
|
||||
@@ -14,18 +14,18 @@ use egui::{
|
||||
use globset::Glob;
|
||||
use objdiff_core::{
|
||||
config::{ProjectObject, DEFAULT_WATCH_PATTERNS},
|
||||
diff::{ArmArchVersion, ArmR9Usage, MipsAbi, MipsInstrCategory, X86Formatter},
|
||||
diff::{
|
||||
ConfigEnum, ConfigEnumVariantInfo, ConfigPropertyId, ConfigPropertyKind,
|
||||
ConfigPropertyValue, CONFIG_GROUPS,
|
||||
},
|
||||
jobs::{check_update::CheckUpdateResult, Job, JobQueue, JobResult},
|
||||
};
|
||||
use strum::{EnumMessage, VariantArray};
|
||||
|
||||
use crate::{
|
||||
app::{AppConfig, AppState, AppStateRef, ObjectConfig},
|
||||
config::ProjectObjectNode,
|
||||
jobs::{
|
||||
check_update::{start_check_update, CheckUpdateResult},
|
||||
update::start_update,
|
||||
Job, JobQueue, JobResult,
|
||||
},
|
||||
hotkeys,
|
||||
jobs::{start_check_update, start_update},
|
||||
update::RELEASE_URL,
|
||||
views::{
|
||||
appearance::Appearance,
|
||||
@@ -118,11 +118,11 @@ impl ConfigViewState {
|
||||
|
||||
if self.queue_check_update {
|
||||
self.queue_check_update = false;
|
||||
jobs.push_once(Job::CheckUpdate, || start_check_update(ctx));
|
||||
start_check_update(ctx, jobs);
|
||||
}
|
||||
|
||||
if let Some(bin_name) = self.queue_update.take() {
|
||||
jobs.push_once(Job::Update, || start_update(ctx, bin_name));
|
||||
start_update(ctx, jobs, bin_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -254,7 +254,11 @@ pub fn config_ui(
|
||||
}
|
||||
} else {
|
||||
let had_search = !config_state.object_search.is_empty();
|
||||
egui::TextEdit::singleline(&mut config_state.object_search).hint_text("Filter").ui(ui);
|
||||
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();
|
||||
}
|
||||
|
||||
let mut root_open = None;
|
||||
let mut node_open = NodeOpen::Default;
|
||||
@@ -872,121 +876,102 @@ 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()
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
_ => panic!("Incompatible property kind and value"),
|
||||
}
|
||||
changed
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
});
|
||||
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;
|
||||
}
|
||||
}
|
||||
});
|
||||
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;
|
||||
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;
|
||||
}
|
||||
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() {
|
||||
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 {
|
||||
state.queue_reload = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,11 +7,14 @@ use objdiff_core::{
|
||||
};
|
||||
use time::format_description;
|
||||
|
||||
use crate::views::{
|
||||
appearance::Appearance,
|
||||
column_layout::{render_header, render_table},
|
||||
symbol_diff::{DiffViewAction, DiffViewNavigation, DiffViewState},
|
||||
write_text,
|
||||
use crate::{
|
||||
hotkeys,
|
||||
views::{
|
||||
appearance::Appearance,
|
||||
column_layout::{render_header, render_table},
|
||||
symbol_diff::{DiffViewAction, DiffViewNavigation, DiffViewState},
|
||||
write_text,
|
||||
},
|
||||
};
|
||||
|
||||
const BYTES_PER_ROW: usize = 16;
|
||||
@@ -176,6 +179,8 @@ fn data_table_ui(
|
||||
let left_diffs = left_section.map(|(_, section)| split_diffs(§ion.data_diff));
|
||||
let right_diffs = right_section.map(|(_, section)| split_diffs(§ion.data_diff));
|
||||
|
||||
hotkeys::check_scroll_hotkeys(ui, true);
|
||||
|
||||
render_table(ui, available_width, 2, config.code_font.size, total_rows, |row, column| {
|
||||
let i = row.index();
|
||||
let address = i * BYTES_PER_ROW;
|
||||
@@ -213,8 +218,8 @@ pub fn data_diff_ui(
|
||||
let right_ctx = SectionDiffContext::new(result.second_obj.as_ref(), section_name);
|
||||
|
||||
// If both sides are missing a symbol, switch to symbol diff view
|
||||
if !right_ctx.map_or(false, |ctx| ctx.has_section())
|
||||
&& !left_ctx.map_or(false, |ctx| ctx.has_section())
|
||||
if !right_ctx.is_some_and(|ctx| ctx.has_section())
|
||||
&& !left_ctx.is_some_and(|ctx| ctx.has_section())
|
||||
{
|
||||
return Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
|
||||
}
|
||||
@@ -224,7 +229,7 @@ pub fn data_diff_ui(
|
||||
render_header(ui, available_width, 2, |ui, column| {
|
||||
if column == 0 {
|
||||
// Left column
|
||||
if ui.button("⏴ Back").clicked() {
|
||||
if ui.button("⏴ Back").clicked() || hotkeys::back_pressed(ui.ctx()) {
|
||||
ret = Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
|
||||
}
|
||||
|
||||
|
||||
@@ -5,13 +5,16 @@ use objdiff_core::{
|
||||
};
|
||||
use time::format_description;
|
||||
|
||||
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,
|
||||
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,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -101,7 +104,7 @@ pub fn extab_diff_ui(
|
||||
let right_diff_symbol = right_ctx.and_then(|ctx| {
|
||||
ctx.symbol_ref.and_then(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).target_symbol)
|
||||
});
|
||||
if left_diff_symbol.is_some() && right_ctx.map_or(false, |ctx| !ctx.has_symbol()) {
|
||||
if left_diff_symbol.is_some() && right_ctx.is_some_and(|ctx| !ctx.has_symbol()) {
|
||||
let (right_section, right_symbol) =
|
||||
right_ctx.unwrap().obj.section_symbol(left_diff_symbol.unwrap());
|
||||
let symbol_ref = SymbolRefByName::new(right_symbol, right_section);
|
||||
@@ -111,7 +114,7 @@ pub fn extab_diff_ui(
|
||||
left_symbol: state.symbol_state.left_symbol.clone(),
|
||||
right_symbol: Some(symbol_ref),
|
||||
}));
|
||||
} else if right_diff_symbol.is_some() && left_ctx.map_or(false, |ctx| !ctx.has_symbol()) {
|
||||
} else if right_diff_symbol.is_some() && left_ctx.is_some_and(|ctx| !ctx.has_symbol()) {
|
||||
let (left_section, left_symbol) =
|
||||
left_ctx.unwrap().obj.section_symbol(right_diff_symbol.unwrap());
|
||||
let symbol_ref = SymbolRefByName::new(left_symbol, left_section);
|
||||
@@ -124,8 +127,8 @@ pub fn extab_diff_ui(
|
||||
}
|
||||
|
||||
// If both sides are missing a symbol, switch to symbol diff view
|
||||
if right_ctx.map_or(false, |ctx| !ctx.has_symbol())
|
||||
&& left_ctx.map_or(false, |ctx| !ctx.has_symbol())
|
||||
if right_ctx.is_some_and(|ctx| !ctx.has_symbol())
|
||||
&& left_ctx.is_some_and(|ctx| !ctx.has_symbol())
|
||||
{
|
||||
return Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
|
||||
}
|
||||
@@ -136,7 +139,7 @@ pub fn extab_diff_ui(
|
||||
if column == 0 {
|
||||
// Left column
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("⏴ Back").clicked() {
|
||||
if ui.button("⏴ Back").clicked() || hotkeys::back_pressed(ui.ctx()) {
|
||||
ret = Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
|
||||
}
|
||||
ui.separator();
|
||||
@@ -144,7 +147,7 @@ pub fn extab_diff_ui(
|
||||
.add_enabled(
|
||||
!state.scratch_running
|
||||
&& state.scratch_available
|
||||
&& left_ctx.map_or(false, |ctx| ctx.has_symbol()),
|
||||
&& left_ctx.is_some_and(|ctx| ctx.has_symbol()),
|
||||
egui::Button::new("📲 decomp.me"),
|
||||
)
|
||||
.on_hover_text_at_pointer("Create a new scratch on decomp.me (beta)")
|
||||
@@ -232,6 +235,8 @@ pub fn extab_diff_ui(
|
||||
}
|
||||
});
|
||||
|
||||
hotkeys::check_scroll_hotkeys(ui, true);
|
||||
|
||||
// Table
|
||||
render_strips(ui, available_width, 2, |ui, column| {
|
||||
if column == 0 {
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
use std::default::Default;
|
||||
use std::{cmp::Ordering, default::Default};
|
||||
|
||||
use egui::{text::LayoutJob, Id, Label, Response, RichText, Sense, Widget};
|
||||
use egui::{text::LayoutJob, Id, Label, Layout, Response, RichText, Sense, Widget};
|
||||
use egui_extras::TableRow;
|
||||
use objdiff_core::{
|
||||
arch::ObjArch,
|
||||
diff::{
|
||||
display::{display_diff, DiffText, HighlightKind},
|
||||
ObjDiff, ObjInsDiff, ObjInsDiffKind,
|
||||
@@ -15,12 +14,15 @@ use objdiff_core::{
|
||||
};
|
||||
use time::format_description;
|
||||
|
||||
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,
|
||||
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,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -77,7 +79,7 @@ impl FunctionViewState {
|
||||
|
||||
fn ins_hover_ui(
|
||||
ui: &mut egui::Ui,
|
||||
arch: &dyn ObjArch,
|
||||
obj: &ObjInfo,
|
||||
section: &ObjSection,
|
||||
ins: &ObjIns,
|
||||
symbol: &ObjSymbol,
|
||||
@@ -120,28 +122,48 @@ fn ins_hover_ui(
|
||||
}
|
||||
|
||||
if let Some(reloc) = &ins.reloc {
|
||||
ui.label(format!("Relocation type: {}", arch.display_reloc(reloc.flags)));
|
||||
ui.colored_label(appearance.highlight_color, format!("Name: {}", reloc.target.name));
|
||||
if let Some(section) = &reloc.target_section {
|
||||
ui.colored_label(appearance.highlight_color, format!("Section: {section}"));
|
||||
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),
|
||||
);
|
||||
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(
|
||||
appearance.highlight_color,
|
||||
format!("Section: {}", section.name),
|
||||
);
|
||||
}
|
||||
ui.colored_label(
|
||||
appearance.highlight_color,
|
||||
format!("Address: {:x}", reloc.target.address),
|
||||
format!("Address: {:x}{}", reloc.target.address, addend_str),
|
||||
);
|
||||
ui.colored_label(
|
||||
appearance.highlight_color,
|
||||
format!("Size: {:x}", reloc.target.size),
|
||||
);
|
||||
if let Some(s) = arch
|
||||
.guess_data_type(ins)
|
||||
.and_then(|ty| arch.display_data_type(ty, &reloc.target.bytes))
|
||||
{
|
||||
ui.colored_label(appearance.highlight_color, s);
|
||||
if reloc.addend >= 0 && reloc.target.bytes.len() > reloc.addend as usize {
|
||||
if let Some(s) = obj.arch.guess_data_type(ins).and_then(|ty| {
|
||||
obj.arch.display_data_type(ty, &reloc.target.bytes[reloc.addend as usize..])
|
||||
}) {
|
||||
ui.colored_label(appearance.highlight_color, s);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ui.colored_label(appearance.highlight_color, "Extern".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(decoded) = rlwinmdec::decode(&ins.formatted) {
|
||||
ui.colored_label(appearance.highlight_color, decoded.trim());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -285,10 +307,14 @@ fn diff_text_ui(
|
||||
base_color = appearance.diff_colors[diff.idx % appearance.diff_colors.len()]
|
||||
}
|
||||
}
|
||||
DiffText::Symbol(sym) => {
|
||||
DiffText::Symbol(sym, diff) => {
|
||||
let name = sym.demangled_name.as_ref().unwrap_or(&sym.name);
|
||||
label_text = name.clone();
|
||||
base_color = appearance.emphasized_text_color;
|
||||
if let Some(diff) = diff {
|
||||
base_color = appearance.diff_colors[diff.idx % appearance.diff_colors.len()]
|
||||
} else {
|
||||
base_color = appearance.emphasized_text_color;
|
||||
}
|
||||
}
|
||||
DiffText::Spacing(n) => {
|
||||
ui.add_space(n as f32 * space_width);
|
||||
@@ -370,7 +396,7 @@ fn asm_col_ui(
|
||||
if let Some(ins) = &ins_diff.ins {
|
||||
response.context_menu(|ui| ins_context_menu(ui, section, ins, symbol));
|
||||
response.on_hover_ui_at_pointer(|ui| {
|
||||
ins_hover_ui(ui, ctx.obj.arch.as_ref(), section, ins, symbol, appearance)
|
||||
ins_hover_ui(ui, ctx.obj, section, ins, symbol, appearance)
|
||||
})
|
||||
} else {
|
||||
response
|
||||
@@ -388,6 +414,7 @@ fn asm_col_ui(
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
fn asm_table_ui(
|
||||
ui: &mut egui::Ui,
|
||||
available_width: f32,
|
||||
@@ -396,6 +423,7 @@ 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| {
|
||||
@@ -421,6 +449,7 @@ 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,
|
||||
@@ -456,6 +485,7 @@ 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,
|
||||
@@ -484,6 +514,7 @@ fn asm_table_ui(
|
||||
SymbolFilter::Mapping(right_symbol_ref),
|
||||
appearance,
|
||||
column,
|
||||
open_sections.0,
|
||||
) {
|
||||
match action {
|
||||
DiffViewAction::Navigate(DiffViewNavigation {
|
||||
@@ -501,9 +532,6 @@ fn asm_table_ui(
|
||||
SymbolRefByName::new(right_symbol, right_section),
|
||||
));
|
||||
}
|
||||
DiffViewAction::SetSymbolHighlight(_, _) => {
|
||||
// Ignore
|
||||
}
|
||||
_ => {
|
||||
ret = Some(action);
|
||||
}
|
||||
@@ -516,6 +544,7 @@ 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,
|
||||
@@ -544,6 +573,7 @@ fn asm_table_ui(
|
||||
SymbolFilter::Mapping(left_symbol_ref),
|
||||
appearance,
|
||||
column,
|
||||
open_sections.1,
|
||||
) {
|
||||
match action {
|
||||
DiffViewAction::Navigate(DiffViewNavigation {
|
||||
@@ -561,9 +591,6 @@ fn asm_table_ui(
|
||||
right_symbol_ref,
|
||||
));
|
||||
}
|
||||
DiffViewAction::SetSymbolHighlight(_, _) => {
|
||||
// Ignore
|
||||
}
|
||||
_ => {
|
||||
ret = Some(action);
|
||||
}
|
||||
@@ -629,7 +656,7 @@ pub fn function_diff_ui(
|
||||
let right_diff_symbol = right_ctx.and_then(|ctx| {
|
||||
ctx.symbol_ref.and_then(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).target_symbol)
|
||||
});
|
||||
if left_diff_symbol.is_some() && right_ctx.map_or(false, |ctx| !ctx.has_symbol()) {
|
||||
if left_diff_symbol.is_some() && right_ctx.is_some_and(|ctx| !ctx.has_symbol()) {
|
||||
let (right_section, right_symbol) =
|
||||
right_ctx.unwrap().obj.section_symbol(left_diff_symbol.unwrap());
|
||||
let symbol_ref = SymbolRefByName::new(right_symbol, right_section);
|
||||
@@ -639,7 +666,7 @@ pub fn function_diff_ui(
|
||||
left_symbol: state.symbol_state.left_symbol.clone(),
|
||||
right_symbol: Some(symbol_ref),
|
||||
}));
|
||||
} else if right_diff_symbol.is_some() && left_ctx.map_or(false, |ctx| !ctx.has_symbol()) {
|
||||
} else if right_diff_symbol.is_some() && left_ctx.is_some_and(|ctx| !ctx.has_symbol()) {
|
||||
let (left_section, left_symbol) =
|
||||
left_ctx.unwrap().obj.section_symbol(right_diff_symbol.unwrap());
|
||||
let symbol_ref = SymbolRefByName::new(left_symbol, left_section);
|
||||
@@ -652,19 +679,20 @@ pub fn function_diff_ui(
|
||||
}
|
||||
|
||||
// If both sides are missing a symbol, switch to symbol diff view
|
||||
if right_ctx.map_or(false, |ctx| !ctx.has_symbol())
|
||||
&& left_ctx.map_or(false, |ctx| !ctx.has_symbol())
|
||||
if right_ctx.is_some_and(|ctx| !ctx.has_symbol())
|
||||
&& left_ctx.is_some_and(|ctx| !ctx.has_symbol())
|
||||
{
|
||||
return Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
|
||||
}
|
||||
|
||||
// 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() {
|
||||
if ui.button("⏴ Back").clicked() || hotkeys::back_pressed(ui.ctx()) {
|
||||
ret = Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
|
||||
}
|
||||
ui.separator();
|
||||
@@ -672,7 +700,7 @@ pub fn function_diff_ui(
|
||||
.add_enabled(
|
||||
!state.scratch_running
|
||||
&& state.scratch_available
|
||||
&& left_ctx.map_or(false, |ctx| ctx.has_symbol()),
|
||||
&& left_ctx.is_some_and(|ctx| ctx.has_symbol()),
|
||||
egui::Button::new("📲 decomp.me"),
|
||||
)
|
||||
.on_hover_text_at_pointer("Create a new scratch on decomp.me (beta)")
|
||||
@@ -696,11 +724,12 @@ pub fn function_diff_ui(
|
||||
.font(appearance.code_font.clone())
|
||||
.color(appearance.highlight_color),
|
||||
);
|
||||
if right_ctx.map_or(false, |m| m.has_symbol())
|
||||
&& ui
|
||||
if right_ctx.is_some_and(|m| m.has_symbol())
|
||||
&& (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()));
|
||||
@@ -712,11 +741,24 @@ pub fn function_diff_ui(
|
||||
.font(appearance.code_font.clone())
|
||||
.color(appearance.replace_color),
|
||||
);
|
||||
ui.label(
|
||||
RichText::new("Choose target symbol")
|
||||
.font(appearance.code_font.clone())
|
||||
.color(appearance.highlight_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);
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
} else if column == 1 {
|
||||
// Right column
|
||||
@@ -766,7 +808,7 @@ pub fn function_diff_ui(
|
||||
.color(match_color_for_symbol(match_percent, appearance)),
|
||||
);
|
||||
}
|
||||
if left_ctx.map_or(false, |m| m.has_symbol()) {
|
||||
if left_ctx.is_some_and(|m| m.has_symbol()) {
|
||||
ui.separator();
|
||||
if ui
|
||||
.button("Change base")
|
||||
@@ -774,6 +816,7 @@ 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()));
|
||||
@@ -787,11 +830,24 @@ pub fn function_diff_ui(
|
||||
.font(appearance.code_font.clone())
|
||||
.color(appearance.replace_color),
|
||||
);
|
||||
ui.label(
|
||||
RichText::new("Choose base symbol")
|
||||
.font(appearance.code_font.clone())
|
||||
.color(appearance.highlight_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);
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -809,6 +865,7 @@ pub fn function_diff_ui(
|
||||
appearance,
|
||||
&state.function_state,
|
||||
&state.symbol_state,
|
||||
open_sections,
|
||||
)
|
||||
})
|
||||
.inner
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use egui::{ProgressBar, RichText, Widget};
|
||||
use objdiff_core::jobs::{JobQueue, JobStatus};
|
||||
|
||||
use crate::{
|
||||
jobs::{JobQueue, JobStatus},
|
||||
views::appearance::Appearance,
|
||||
};
|
||||
use crate::views::appearance::Appearance;
|
||||
|
||||
pub fn jobs_ui(ui: &mut egui::Ui, jobs: &mut JobQueue, appearance: &Appearance) {
|
||||
if ui.button("Clear").clicked() {
|
||||
|
||||
@@ -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(demangled) = rlwinmdec::decode(&state.text) {
|
||||
if let Some(decoded) = rlwinmdec::decode(&state.text) {
|
||||
ui.scope(|ui| {
|
||||
ui.style_mut().override_text_style = Some(TextStyle::Monospace);
|
||||
ui.colored_label(appearance.replace_color, &demangled);
|
||||
ui.colored_label(appearance.replace_color, decoded.trim());
|
||||
});
|
||||
if ui.button("Copy").clicked() {
|
||||
ui.output_mut(|output| output.copied_text = demangled);
|
||||
ui.output_mut(|output| output.copied_text = decoded);
|
||||
}
|
||||
} else {
|
||||
ui.scope(|ui| {
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
use std::{collections::BTreeMap, mem::take};
|
||||
use std::{collections::BTreeMap, mem::take, ops::Bound};
|
||||
|
||||
use egui::{
|
||||
text::LayoutJob, CollapsingHeader, Color32, Id, OpenUrl, ScrollArea, SelectableLabel, TextEdit,
|
||||
Ui, Widget,
|
||||
style::ScrollAnimation, text::LayoutJob, CollapsingHeader, Color32, Id, Layout, OpenUrl,
|
||||
ScrollArea, SelectableLabel, TextEdit, Ui, Widget,
|
||||
};
|
||||
use objdiff_core::{
|
||||
arch::ObjArch,
|
||||
build::BuildStatus,
|
||||
diff::{display::HighlightKind, ObjDiff, ObjSymbolDiff},
|
||||
obj::{ObjInfo, ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlags, SymbolRef},
|
||||
jobs::{create_scratch::CreateScratchResult, objdiff::ObjDiffResult, Job, JobQueue, JobResult},
|
||||
obj::{
|
||||
ObjInfo, ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlags, SymbolRef, SECTION_COMMON,
|
||||
},
|
||||
};
|
||||
use regex::{Regex, RegexBuilder};
|
||||
|
||||
use crate::{
|
||||
app::AppStateRef,
|
||||
jobs::{
|
||||
create_scratch::{start_create_scratch, CreateScratchConfig, CreateScratchResult},
|
||||
objdiff::{BuildStatus, ObjDiffResult},
|
||||
Job, JobQueue, JobResult,
|
||||
},
|
||||
hotkeys,
|
||||
jobs::{is_create_scratch_available, start_create_scratch},
|
||||
views::{
|
||||
appearance::Appearance,
|
||||
column_layout::{render_header, render_strips},
|
||||
@@ -54,8 +55,8 @@ pub enum DiffViewAction {
|
||||
Build,
|
||||
/// Navigate to a new diff view
|
||||
Navigate(DiffViewNavigation),
|
||||
/// Set the highlighted symbols in the symbols view
|
||||
SetSymbolHighlight(Option<SymbolRef>, Option<SymbolRef>),
|
||||
/// Set the highlighted symbols in the symbols view, optionally scrolling them into view.
|
||||
SetSymbolHighlight(Option<SymbolRef>, Option<SymbolRef>, bool),
|
||||
/// Set the symbols view search filter
|
||||
SetSearch(String),
|
||||
/// Submit the current function to decomp.me
|
||||
@@ -133,6 +134,7 @@ 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,
|
||||
@@ -178,7 +180,7 @@ impl DiffViewState {
|
||||
} else {
|
||||
self.source_path_available = false;
|
||||
}
|
||||
self.scratch_available = CreateScratchConfig::is_available(&state.config);
|
||||
self.scratch_available = is_create_scratch_available(&state.config);
|
||||
self.object_name =
|
||||
state.config.selected_obj.as_ref().map(|o| o.name.clone()).unwrap_or_default();
|
||||
}
|
||||
@@ -195,6 +197,9 @@ 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;
|
||||
};
|
||||
@@ -209,7 +214,6 @@ 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;
|
||||
};
|
||||
@@ -245,8 +249,9 @@ impl DiffViewState {
|
||||
self.post_build_nav = Some(nav);
|
||||
}
|
||||
}
|
||||
DiffViewAction::SetSymbolHighlight(left, right) => {
|
||||
DiffViewAction::SetSymbolHighlight(left, right, autoscroll) => {
|
||||
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() {
|
||||
@@ -263,14 +268,7 @@ impl DiffViewState {
|
||||
let Ok(state) = state.read() else {
|
||||
return;
|
||||
};
|
||||
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}");
|
||||
}
|
||||
}
|
||||
start_create_scratch(ctx, jobs, &state, function_name);
|
||||
}
|
||||
DiffViewAction::OpenSourcePath => {
|
||||
let Ok(state) = state.read() else {
|
||||
@@ -532,7 +530,15 @@ fn symbol_ui(
|
||||
ret = Some(DiffViewAction::Navigate(result));
|
||||
}
|
||||
});
|
||||
if response.clicked() {
|
||||
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 let Some(section) = section {
|
||||
match section.kind {
|
||||
ObjSectionKind::Code => {
|
||||
@@ -559,20 +565,18 @@ fn symbol_ui(
|
||||
}
|
||||
}
|
||||
} else if response.hovered() {
|
||||
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),
|
||||
)
|
||||
}
|
||||
ret = Some(if column == 0 {
|
||||
DiffViewAction::SetSymbolHighlight(
|
||||
Some(symbol_diff.symbol_ref),
|
||||
symbol_diff.target_symbol,
|
||||
false,
|
||||
)
|
||||
} else {
|
||||
DiffViewAction::SetSymbolHighlight(None, None)
|
||||
DiffViewAction::SetSymbolHighlight(
|
||||
symbol_diff.target_symbol,
|
||||
Some(symbol_diff.symbol_ref),
|
||||
false,
|
||||
)
|
||||
});
|
||||
}
|
||||
ret
|
||||
@@ -601,6 +605,7 @@ pub enum SymbolFilter<'a> {
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
pub fn symbol_list_ui(
|
||||
ui: &mut Ui,
|
||||
ctx: SymbolDiffContext<'_>,
|
||||
@@ -609,6 +614,7 @@ 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| {
|
||||
@@ -646,16 +652,68 @@ 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);
|
||||
|
||||
// Skip sections with all symbols filtered out
|
||||
if mapping.keys().any(|symbol_ref| symbol_ref.section_idx == usize::MAX) {
|
||||
if mapping.keys().any(|symbol_ref| symbol_ref.section_idx == SECTION_COMMON) {
|
||||
CollapsingHeader::new(".comm").default_open(true).show(ui, |ui| {
|
||||
for (symbol_ref, symbol_diff) in mapping
|
||||
.iter()
|
||||
.filter(|(symbol_ref, _)| symbol_ref.section_idx == usize::MAX)
|
||||
.filter(|(symbol_ref, _)| symbol_ref.section_idx == SECTION_COMMON)
|
||||
{
|
||||
let symbol = ctx.obj.section_symbol(*symbol_ref).1;
|
||||
if let Some(result) = symbol_ui(
|
||||
@@ -710,6 +768,7 @@ 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
|
||||
@@ -817,6 +876,7 @@ 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
|
||||
@@ -835,10 +895,25 @@ pub fn symbol_diff_ui(
|
||||
}
|
||||
});
|
||||
|
||||
let mut search = state.search.clone();
|
||||
if TextEdit::singleline(&mut search).hint_text("Filter symbols").ui(ui).changed() {
|
||||
ret = Some(DiffViewAction::SetSearch(search));
|
||||
}
|
||||
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);
|
||||
}
|
||||
})
|
||||
});
|
||||
} else if column == 1 {
|
||||
// Right column
|
||||
ui.horizontal(|ui| {
|
||||
@@ -870,9 +945,20 @@ pub fn symbol_diff_ui(
|
||||
}
|
||||
});
|
||||
|
||||
if ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() {
|
||||
ret = Some(DiffViewAction::Build);
|
||||
}
|
||||
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);
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -897,6 +983,7 @@ pub fn symbol_diff_ui(
|
||||
filter,
|
||||
appearance,
|
||||
column,
|
||||
open_sections.0,
|
||||
) {
|
||||
ret = Some(result);
|
||||
}
|
||||
@@ -921,6 +1008,7 @@ pub fn symbol_diff_ui(
|
||||
filter,
|
||||
appearance,
|
||||
column,
|
||||
open_sections.1,
|
||||
) {
|
||||
ret = Some(result);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "objdiff-wasm",
|
||||
"version": "2.0.0",
|
||||
"version": "2.6.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,dwarf,ppc,x86,wasm"
|
||||
"build:wasm": "cd ../objdiff-core && wasm-pack build --out-dir ../objdiff-wasm/pkg --target web -- --features arm,arm64,dwarf,config,ppc,x86,wasm"
|
||||
},
|
||||
"dependencies": {
|
||||
"@protobuf-ts/runtime": "^2.9.4"
|
||||
|
||||
@@ -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, config?: DiffObjConfig): Promise<DiffResult> {
|
||||
export async function runDiff(left: Uint8Array | undefined, right: Uint8Array | undefined, diff_config?: DiffObjConfig): Promise<DiffResult> {
|
||||
const data = await defer<Uint8Array>({
|
||||
type: 'run_diff_proto',
|
||||
left,
|
||||
right,
|
||||
config
|
||||
diff_config
|
||||
});
|
||||
const parseStart = performance.now();
|
||||
const result = DiffResult.fromBinary(data, {readUnknownField: false});
|
||||
@@ -194,12 +194,17 @@ 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;
|
||||
const diff_index = diff.arg_diff[i]?.diff_index;
|
||||
let diff_index: number | undefined;
|
||||
if (arg.oneofKind !== 'plain_text') {
|
||||
diff_index = diff.arg_diff[arg_diff_idx]?.diff_index;
|
||||
arg_diff_idx++;
|
||||
}
|
||||
switch (arg.oneofKind) {
|
||||
case "plain_text":
|
||||
cb({type: 'basic', text: arg.plain_text, diff_index});
|
||||
|
||||
@@ -38,13 +38,15 @@ async function initIfNeeded() {
|
||||
// return exports.run_diff_json(left, right, cfg);
|
||||
// }
|
||||
|
||||
async function run_diff_proto({left, right, config}: {
|
||||
async function run_diff_proto({left, right, diff_config, mapping_config}: {
|
||||
left: Uint8Array | undefined,
|
||||
right: Uint8Array | undefined,
|
||||
config?: exports.DiffObjConfig,
|
||||
diff_config?: exports.DiffObjConfig,
|
||||
mapping_config?: exports.MappingConfig,
|
||||
}): Promise<Uint8Array> {
|
||||
config = config || {};
|
||||
return exports.run_diff_proto(left, right, config);
|
||||
diff_config = diff_config || {};
|
||||
mapping_config = mapping_config || {};
|
||||
return exports.run_diff_proto(left, right, diff_config, mapping_config);
|
||||
}
|
||||
|
||||
export type AnyHandlerData = HandlerData[keyof HandlerData];
|
||||
@@ -73,12 +75,19 @@ 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);
|
||||
} as OutMessage, {transfer});
|
||||
} else {
|
||||
throw new Error(`No handler for ${data.type}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user