mirror of
https://github.com/encounter/objdiff.git
synced 2025-12-12 14:46:12 +00:00
objdiff-cli diff & report changes, support .splitmeta object section
- Add `objdiff-cli report changes` for diffing two reports - Unify some click-to-highlight logic between CLI and GUI - Load .splitmeta section for extra object metadata (original virtual addr, etc) - More work on objdiff-cli diff
This commit is contained in:
@@ -20,10 +20,11 @@ use crossterm::{
|
||||
};
|
||||
use event::KeyModifiers;
|
||||
use objdiff_core::{
|
||||
config::ProjectConfig,
|
||||
diff,
|
||||
diff::display::{display_diff, DiffText},
|
||||
diff::display::{display_diff, DiffText, HighlightKind},
|
||||
obj,
|
||||
obj::{ObjInfo, ObjInsArgValue, ObjInsDiffKind, ObjSection, ObjSectionKind, ObjSymbol},
|
||||
obj::{ObjInfo, ObjInsDiffKind, ObjSectionKind, ObjSymbol},
|
||||
};
|
||||
|
||||
use crate::util::term::crossterm_panic_handler;
|
||||
@@ -32,33 +33,71 @@ use crate::util::term::crossterm_panic_handler;
|
||||
/// Diff two object files.
|
||||
#[argp(subcommand, name = "diff")]
|
||||
pub struct Args {
|
||||
#[argp(positional)]
|
||||
#[argp(option, short = '1')]
|
||||
/// Target object file
|
||||
target: PathBuf,
|
||||
#[argp(positional)]
|
||||
target: Option<PathBuf>,
|
||||
#[argp(option, short = '2')]
|
||||
/// Base object file
|
||||
base: PathBuf,
|
||||
base: Option<PathBuf>,
|
||||
#[argp(option, short = 'p')]
|
||||
/// Project directory
|
||||
project: Option<PathBuf>,
|
||||
#[argp(option, short = 'u')]
|
||||
/// Unit name within project
|
||||
unit: Option<String>,
|
||||
#[argp(option, short = 's')]
|
||||
/// Function symbol to diff
|
||||
symbol: String,
|
||||
}
|
||||
|
||||
pub fn run(args: Args) -> Result<()> {
|
||||
let mut target = obj::elf::read(&args.target)
|
||||
.with_context(|| format!("Loading {}", args.target.display()))?;
|
||||
let mut base =
|
||||
obj::elf::read(&args.base).with_context(|| format!("Loading {}", args.base.display()))?;
|
||||
let config = diff::DiffObjConfig::default();
|
||||
diff::diff_objs(&config, Some(&mut target), Some(&mut base))?;
|
||||
|
||||
let left_sym = find_function(&target, &args.symbol);
|
||||
let right_sym = find_function(&base, &args.symbol);
|
||||
let max_len = match (left_sym, right_sym) {
|
||||
(Some((_, l)), Some((_, r))) => l.instructions.len().max(r.instructions.len()),
|
||||
(Some((_, l)), None) => l.instructions.len(),
|
||||
(None, Some((_, r))) => r.instructions.len(),
|
||||
(None, None) => bail!("Symbol not found: {}", args.symbol),
|
||||
let (target_path, base_path, project_config) =
|
||||
match (&args.target, &args.base, &args.project, &args.unit) {
|
||||
(Some(t), Some(b), _, _) => (Some(t.clone()), Some(b.clone()), None),
|
||||
(_, _, Some(p), Some(u)) => {
|
||||
let Some((project_config, project_config_info)) =
|
||||
objdiff_core::config::try_project_config(p)
|
||||
else {
|
||||
bail!("Project config not found in {}", p.display())
|
||||
};
|
||||
let mut project_config = project_config.with_context(|| {
|
||||
format!("Reading project config {}", project_config_info.path.display())
|
||||
})?;
|
||||
let Some(object) = project_config.objects.iter_mut().find(|obj| obj.name() == u)
|
||||
else {
|
||||
bail!("Unit not found: {}", u)
|
||||
};
|
||||
object.resolve_paths(
|
||||
p,
|
||||
project_config.target_dir.as_deref(),
|
||||
project_config.base_dir.as_deref(),
|
||||
);
|
||||
let target_path = object.target_path.clone();
|
||||
let base_path = object.base_path.clone();
|
||||
(target_path, base_path, Some(project_config))
|
||||
}
|
||||
_ => bail!("Either target and base or project and unit must be specified"),
|
||||
};
|
||||
let mut state = FunctionDiffUi {
|
||||
clear: true,
|
||||
redraw: true,
|
||||
size: (0, 0),
|
||||
click_xy: None,
|
||||
left_highlight: HighlightKind::None,
|
||||
right_highlight: HighlightKind::None,
|
||||
skip: 0,
|
||||
y_offset: 2,
|
||||
per_page: 0,
|
||||
max_len: 0,
|
||||
symbol_name: args.symbol.clone(),
|
||||
target_path,
|
||||
base_path,
|
||||
project_config,
|
||||
left_sym: None,
|
||||
right_sym: None,
|
||||
reload_time: time::OffsetDateTime::now_local()?,
|
||||
};
|
||||
state.reload()?;
|
||||
|
||||
crossterm_panic_handler();
|
||||
enable_raw_mode()?;
|
||||
@@ -69,175 +108,26 @@ pub fn run(args: Args) -> Result<()> {
|
||||
Hide,
|
||||
EnableMouseCapture,
|
||||
)?;
|
||||
state.size = terminal_size()?;
|
||||
|
||||
let mut clear = true;
|
||||
let mut redraw = true;
|
||||
let mut skip = 0;
|
||||
let mut click_xy = None;
|
||||
let mut highlight = HighlightKind::None;
|
||||
let (mut sx, mut sy) = terminal_size()?;
|
||||
loop {
|
||||
let y_offset = 2;
|
||||
let per_page = sy as usize - y_offset;
|
||||
if redraw {
|
||||
let mut w = stdout().lock();
|
||||
if clear {
|
||||
crossterm::queue!(w, Clear(ClearType::All))?;
|
||||
}
|
||||
crossterm::queue!(
|
||||
w,
|
||||
MoveTo(0, 0),
|
||||
PrintStyledContent(args.symbol.clone().with(Color::White)),
|
||||
MoveTo(0, 1),
|
||||
PrintStyledContent(" ".repeat(sx as usize).underlined()),
|
||||
MoveTo(0, 1),
|
||||
PrintStyledContent("TARGET ".underlined()),
|
||||
MoveTo(sx / 2, 0),
|
||||
PrintStyledContent("Last built: 18:24:20".with(Color::White)),
|
||||
MoveTo(sx / 2, 1),
|
||||
PrintStyledContent("BASE ".underlined()),
|
||||
)?;
|
||||
if let Some(percent) = right_sym.and_then(|(_, s)| s.match_percent) {
|
||||
crossterm::queue!(
|
||||
w,
|
||||
PrintStyledContent(
|
||||
format!("{:.2}%", percent).with(match_percent_color(percent)).underlined()
|
||||
)
|
||||
)?;
|
||||
}
|
||||
|
||||
if skip > max_len - per_page {
|
||||
skip = max_len - per_page;
|
||||
}
|
||||
let mut new_highlight = None;
|
||||
if let Some((_, symbol)) = left_sym {
|
||||
let h = print_sym(
|
||||
&mut w,
|
||||
symbol,
|
||||
0,
|
||||
y_offset as u16,
|
||||
sx / 2 - 1,
|
||||
sy,
|
||||
skip,
|
||||
&mut highlight,
|
||||
click_xy,
|
||||
)?;
|
||||
if let Some(h) = h {
|
||||
new_highlight = Some(h);
|
||||
let reload = loop {
|
||||
if state.redraw {
|
||||
state.draw()?;
|
||||
if state.redraw {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if let Some((_, symbol)) = right_sym {
|
||||
let h = print_sym(
|
||||
&mut w,
|
||||
symbol,
|
||||
sx / 2,
|
||||
y_offset as u16,
|
||||
sx,
|
||||
sy,
|
||||
skip,
|
||||
&mut highlight,
|
||||
click_xy,
|
||||
)?;
|
||||
if let Some(h) = h {
|
||||
new_highlight = Some(h);
|
||||
}
|
||||
match state.handle_event(event::read()?) {
|
||||
FunctionDiffResult::Break => break false,
|
||||
FunctionDiffResult::Continue => {}
|
||||
FunctionDiffResult::Reload => break true,
|
||||
}
|
||||
w.flush()?;
|
||||
if let Some(new_highlight) = new_highlight {
|
||||
highlight = new_highlight;
|
||||
redraw = true;
|
||||
click_xy = None;
|
||||
clear = false;
|
||||
continue; // Redraw now
|
||||
} else {
|
||||
redraw = false;
|
||||
click_xy = None;
|
||||
clear = true;
|
||||
}
|
||||
}
|
||||
|
||||
match event::read()? {
|
||||
Event::Key(event)
|
||||
if matches!(event.kind, KeyEventKind::Press | KeyEventKind::Repeat) =>
|
||||
{
|
||||
match event.code {
|
||||
// Quit
|
||||
KeyCode::Esc | KeyCode::Char('q') => break,
|
||||
// Page up
|
||||
KeyCode::PageUp => {
|
||||
skip = skip.saturating_sub(per_page);
|
||||
redraw = true;
|
||||
}
|
||||
// Page up (shift + space)
|
||||
KeyCode::Char(' ') if event.modifiers.contains(KeyModifiers::SHIFT) => {
|
||||
skip = skip.saturating_sub(per_page);
|
||||
redraw = true;
|
||||
}
|
||||
// Page down
|
||||
KeyCode::Char(' ') | KeyCode::PageDown => {
|
||||
skip += per_page;
|
||||
redraw = true;
|
||||
}
|
||||
KeyCode::Char('f') if event.modifiers.contains(KeyModifiers::CONTROL) => {
|
||||
skip += per_page;
|
||||
redraw = true;
|
||||
}
|
||||
KeyCode::Char('b') if event.modifiers.contains(KeyModifiers::CONTROL) => {
|
||||
skip = skip.saturating_sub(per_page);
|
||||
redraw = true;
|
||||
}
|
||||
KeyCode::Char('d') if event.modifiers.contains(KeyModifiers::CONTROL) => {
|
||||
skip += per_page / 2;
|
||||
redraw = true;
|
||||
}
|
||||
KeyCode::Char('u') if event.modifiers.contains(KeyModifiers::CONTROL) => {
|
||||
skip = skip.saturating_sub(per_page / 2);
|
||||
redraw = true;
|
||||
}
|
||||
// Scroll down
|
||||
KeyCode::Down | KeyCode::Char('j') => {
|
||||
skip += 1;
|
||||
redraw = true;
|
||||
}
|
||||
// Scroll up
|
||||
KeyCode::Up | KeyCode::Char('k') => {
|
||||
skip = skip.saturating_sub(1);
|
||||
redraw = true;
|
||||
}
|
||||
// Scroll to start
|
||||
KeyCode::Char('g') => {
|
||||
skip = 0;
|
||||
redraw = true;
|
||||
}
|
||||
// Scroll to end
|
||||
KeyCode::Char('G') => {
|
||||
skip = max_len;
|
||||
redraw = true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Event::Mouse(event) => match event.kind {
|
||||
MouseEventKind::ScrollDown => {
|
||||
skip += 3;
|
||||
redraw = true;
|
||||
}
|
||||
MouseEventKind::ScrollUp => {
|
||||
skip = skip.saturating_sub(3);
|
||||
redraw = true;
|
||||
}
|
||||
MouseEventKind::Down(MouseButton::Left) => {
|
||||
click_xy = Some((event.column, event.row));
|
||||
redraw = true;
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Event::Resize(x, y) => {
|
||||
sx = x;
|
||||
sy = y;
|
||||
redraw = true;
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
if reload {
|
||||
state.reload()?;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,148 +137,384 @@ pub fn run(args: Args) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn find_function<'a>(obj: &'a ObjInfo, name: &str) -> Option<(&'a ObjSection, &'a ObjSymbol)> {
|
||||
fn find_function(obj: &ObjInfo, name: &str) -> Option<ObjSymbol> {
|
||||
for section in &obj.sections {
|
||||
if section.kind != ObjSectionKind::Code {
|
||||
continue;
|
||||
}
|
||||
for symbol in §ion.symbols {
|
||||
if symbol.name == name {
|
||||
return Some((section, symbol));
|
||||
return Some(symbol.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn print_sym<W>(
|
||||
w: &mut W,
|
||||
symbol: &ObjSymbol,
|
||||
sx: u16,
|
||||
mut sy: u16,
|
||||
max_sx: u16,
|
||||
max_sy: u16,
|
||||
skip: usize,
|
||||
highlight: &mut HighlightKind,
|
||||
#[allow(dead_code)]
|
||||
struct FunctionDiffUi {
|
||||
clear: bool,
|
||||
redraw: bool,
|
||||
size: (u16, u16),
|
||||
click_xy: Option<(u16, u16)>,
|
||||
) -> Result<Option<HighlightKind>>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
let base_addr = symbol.address as u32;
|
||||
let mut new_highlight = None;
|
||||
for ins_diff in symbol.instructions.iter().skip(skip) {
|
||||
let mut sx = sx;
|
||||
if ins_diff.kind != ObjInsDiffKind::None && sx > 2 {
|
||||
crossterm::queue!(w, MoveTo(sx - 2, sy))?;
|
||||
let s = match ins_diff.kind {
|
||||
ObjInsDiffKind::Delete => "< ",
|
||||
ObjInsDiffKind::Insert => "> ",
|
||||
_ => "| ",
|
||||
};
|
||||
crossterm::queue!(w, PrintStyledContent(s.with(Color::DarkGrey)))?;
|
||||
left_highlight: HighlightKind,
|
||||
right_highlight: HighlightKind,
|
||||
skip: usize,
|
||||
y_offset: usize,
|
||||
per_page: usize,
|
||||
max_len: usize,
|
||||
symbol_name: String,
|
||||
target_path: Option<PathBuf>,
|
||||
base_path: Option<PathBuf>,
|
||||
project_config: Option<ProjectConfig>,
|
||||
left_sym: Option<ObjSymbol>,
|
||||
right_sym: Option<ObjSymbol>,
|
||||
reload_time: time::OffsetDateTime,
|
||||
}
|
||||
|
||||
enum FunctionDiffResult {
|
||||
Break,
|
||||
Continue,
|
||||
Reload,
|
||||
}
|
||||
|
||||
impl FunctionDiffUi {
|
||||
fn draw(&mut self) -> Result<()> {
|
||||
let mut w = stdout().lock();
|
||||
if self.clear {
|
||||
crossterm::queue!(w, Clear(ClearType::All))?;
|
||||
}
|
||||
let format = time::format_description::parse("[hour]:[minute]:[second]").unwrap();
|
||||
let reload_time = self.reload_time.format(&format).unwrap();
|
||||
crossterm::queue!(
|
||||
w,
|
||||
MoveTo(0, 0),
|
||||
PrintStyledContent(self.symbol_name.clone().with(Color::White)),
|
||||
MoveTo(0, 1),
|
||||
PrintStyledContent(" ".repeat(self.size.0 as usize).underlined()),
|
||||
MoveTo(0, 1),
|
||||
PrintStyledContent("TARGET ".underlined()),
|
||||
MoveTo(self.size.0 / 2, 0),
|
||||
PrintStyledContent(format!("Last reload: {}", reload_time).with(Color::White)),
|
||||
MoveTo(self.size.0 / 2, 1),
|
||||
PrintStyledContent("BASE ".underlined()),
|
||||
)?;
|
||||
if let Some(percent) = self.right_sym.as_ref().and_then(|s| s.match_percent) {
|
||||
crossterm::queue!(
|
||||
w,
|
||||
PrintStyledContent(
|
||||
format!("{:.2}%", percent).with(match_percent_color(percent)).underlined()
|
||||
)
|
||||
)?;
|
||||
}
|
||||
|
||||
self.per_page = self.size.1 as usize - self.y_offset;
|
||||
let max_skip = self.max_len.saturating_sub(self.per_page);
|
||||
if self.skip > max_skip {
|
||||
self.skip = max_skip;
|
||||
}
|
||||
let mut left_highlight = None;
|
||||
if let Some(symbol) = &self.left_sym {
|
||||
let h = self.print_sym(
|
||||
&mut w,
|
||||
symbol,
|
||||
(0, self.y_offset as u16),
|
||||
(self.size.0 / 2 - 1, self.size.1),
|
||||
&self.left_highlight,
|
||||
)?;
|
||||
if let Some(h) = h {
|
||||
left_highlight = Some(h);
|
||||
}
|
||||
}
|
||||
let mut right_highlight = None;
|
||||
if let Some(symbol) = &self.right_sym {
|
||||
let h = self.print_sym(
|
||||
&mut w,
|
||||
symbol,
|
||||
(self.size.0 / 2, self.y_offset as u16),
|
||||
self.size,
|
||||
&self.right_highlight,
|
||||
)?;
|
||||
if let Some(h) = h {
|
||||
right_highlight = Some(h);
|
||||
}
|
||||
}
|
||||
w.flush()?;
|
||||
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;
|
||||
}
|
||||
self.redraw = true;
|
||||
self.click_xy = None;
|
||||
self.clear = false;
|
||||
} 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;
|
||||
}
|
||||
self.redraw = true;
|
||||
self.click_xy = None;
|
||||
self.clear = false;
|
||||
} else {
|
||||
crossterm::queue!(w, MoveTo(sx, sy))?;
|
||||
}
|
||||
display_diff(ins_diff, base_addr, |text| -> Result<()> {
|
||||
let mut label_text;
|
||||
let mut base_color = match ins_diff.kind {
|
||||
ObjInsDiffKind::None | ObjInsDiffKind::OpMismatch | ObjInsDiffKind::ArgMismatch => {
|
||||
Color::Grey
|
||||
}
|
||||
ObjInsDiffKind::Replace => Color::DarkCyan,
|
||||
ObjInsDiffKind::Delete => Color::DarkRed,
|
||||
ObjInsDiffKind::Insert => Color::DarkGreen,
|
||||
};
|
||||
let mut pad_to = 0;
|
||||
let mut highlight_kind = HighlightKind::None;
|
||||
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::DarkGrey;
|
||||
pad_to = 5;
|
||||
}
|
||||
DiffText::Address(addr) => {
|
||||
label_text = format!("{:x}:", addr);
|
||||
pad_to = 5;
|
||||
highlight_kind = HighlightKind::Address(addr);
|
||||
}
|
||||
DiffText::Opcode(mnemonic, op) => {
|
||||
label_text = mnemonic.to_string();
|
||||
if ins_diff.kind == ObjInsDiffKind::OpMismatch {
|
||||
base_color = Color::Blue;
|
||||
}
|
||||
pad_to = 8;
|
||||
highlight_kind = HighlightKind::Opcode(op);
|
||||
}
|
||||
DiffText::Argument(arg, diff) => {
|
||||
label_text = arg.to_string();
|
||||
if let Some(diff) = diff {
|
||||
base_color = COLOR_ROTATION[diff.idx % COLOR_ROTATION.len()]
|
||||
}
|
||||
highlight_kind = HighlightKind::Arg(arg.clone());
|
||||
}
|
||||
DiffText::BranchTarget(addr) => {
|
||||
label_text = format!("{addr:x}");
|
||||
highlight_kind = HighlightKind::Address(addr);
|
||||
}
|
||||
DiffText::Symbol(sym) => {
|
||||
let name = sym.demangled_name.as_ref().unwrap_or(&sym.name);
|
||||
label_text = name.clone();
|
||||
base_color = Color::White;
|
||||
highlight_kind = HighlightKind::Symbol(name.clone());
|
||||
}
|
||||
DiffText::Spacing(n) => {
|
||||
crossterm::queue!(w, MoveRight(n as u16))?;
|
||||
sx += n as u16;
|
||||
return Ok(());
|
||||
}
|
||||
DiffText::Eol => {
|
||||
sy += 1;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
let len = label_text.len();
|
||||
if sx >= max_sx {
|
||||
return Ok(());
|
||||
}
|
||||
let highlighted = highlight == &highlight_kind;
|
||||
if let Some((cx, cy)) = click_xy {
|
||||
if cx >= sx && cx < sx + len as u16 && cy == sy {
|
||||
if highlighted {
|
||||
new_highlight = Some(HighlightKind::None);
|
||||
} else {
|
||||
new_highlight = Some(highlight_kind);
|
||||
}
|
||||
}
|
||||
}
|
||||
label_text.truncate(max_sx as usize - sx as usize);
|
||||
let mut content = label_text.with(base_color);
|
||||
if highlighted {
|
||||
content = content.on_dark_grey();
|
||||
}
|
||||
crossterm::queue!(w, PrintStyledContent(content))?;
|
||||
sx += len as u16;
|
||||
if pad_to > len {
|
||||
let pad = (pad_to - len) as u16;
|
||||
crossterm::queue!(w, MoveRight(pad))?;
|
||||
sx += pad;
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
if sy >= max_sy {
|
||||
break;
|
||||
self.redraw = false;
|
||||
self.click_xy = None;
|
||||
self.clear = true;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_event(&mut self, event: Event) -> FunctionDiffResult {
|
||||
match event {
|
||||
Event::Key(event)
|
||||
if matches!(event.kind, KeyEventKind::Press | KeyEventKind::Repeat) =>
|
||||
{
|
||||
match event.code {
|
||||
// Quit
|
||||
KeyCode::Esc | KeyCode::Char('q') => return FunctionDiffResult::Break,
|
||||
// Page up
|
||||
KeyCode::PageUp => {
|
||||
self.skip = self.skip.saturating_sub(self.per_page);
|
||||
self.redraw = true;
|
||||
}
|
||||
// Page up (shift + space)
|
||||
KeyCode::Char(' ') if event.modifiers.contains(KeyModifiers::SHIFT) => {
|
||||
self.skip = self.skip.saturating_sub(self.per_page);
|
||||
self.redraw = true;
|
||||
}
|
||||
// Page down
|
||||
KeyCode::Char(' ') | KeyCode::PageDown => {
|
||||
self.skip += self.per_page;
|
||||
self.redraw = true;
|
||||
}
|
||||
// Page down (ctrl + f)
|
||||
KeyCode::Char('f') if event.modifiers.contains(KeyModifiers::CONTROL) => {
|
||||
self.skip += self.per_page;
|
||||
self.redraw = true;
|
||||
}
|
||||
// Page up (ctrl + b)
|
||||
KeyCode::Char('b') if event.modifiers.contains(KeyModifiers::CONTROL) => {
|
||||
self.skip = self.skip.saturating_sub(self.per_page);
|
||||
self.redraw = true;
|
||||
}
|
||||
// Half page down (ctrl + d)
|
||||
KeyCode::Char('d') if event.modifiers.contains(KeyModifiers::CONTROL) => {
|
||||
self.skip += self.per_page / 2;
|
||||
self.redraw = true;
|
||||
}
|
||||
// Half page up (ctrl + u)
|
||||
KeyCode::Char('u') if event.modifiers.contains(KeyModifiers::CONTROL) => {
|
||||
self.skip = self.skip.saturating_sub(self.per_page / 2);
|
||||
self.redraw = true;
|
||||
}
|
||||
// Scroll down
|
||||
KeyCode::Down | KeyCode::Char('j') => {
|
||||
self.skip += 1;
|
||||
self.redraw = true;
|
||||
}
|
||||
// Scroll up
|
||||
KeyCode::Up | KeyCode::Char('k') => {
|
||||
self.skip = self.skip.saturating_sub(1);
|
||||
self.redraw = true;
|
||||
}
|
||||
// Scroll to start
|
||||
KeyCode::Char('g') => {
|
||||
self.skip = 0;
|
||||
self.redraw = true;
|
||||
}
|
||||
// Scroll to end
|
||||
KeyCode::Char('G') => {
|
||||
self.skip = self.max_len;
|
||||
self.redraw = true;
|
||||
}
|
||||
// Reload
|
||||
KeyCode::Char('r') => {
|
||||
self.redraw = true;
|
||||
return FunctionDiffResult::Reload;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Event::Mouse(event) => match event.kind {
|
||||
MouseEventKind::ScrollDown => {
|
||||
self.skip += 3;
|
||||
self.redraw = true;
|
||||
}
|
||||
MouseEventKind::ScrollUp => {
|
||||
self.skip = self.skip.saturating_sub(3);
|
||||
self.redraw = true;
|
||||
}
|
||||
MouseEventKind::Down(MouseButton::Left) => {
|
||||
self.click_xy = Some((event.column, event.row));
|
||||
self.redraw = true;
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Event::Resize(x, y) => {
|
||||
self.size = (x, y);
|
||||
self.redraw = true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
FunctionDiffResult::Continue
|
||||
}
|
||||
|
||||
fn print_sym<W>(
|
||||
&self,
|
||||
w: &mut W,
|
||||
symbol: &ObjSymbol,
|
||||
origin: (u16, u16),
|
||||
max: (u16, u16),
|
||||
highlight: &HighlightKind,
|
||||
) -> Result<Option<HighlightKind>>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
let base_addr = symbol.address as u32;
|
||||
let mut new_highlight = None;
|
||||
let mut sy = origin.1;
|
||||
for ins_diff in symbol.instructions.iter().skip(self.skip) {
|
||||
let mut sx = origin.0;
|
||||
if ins_diff.kind != ObjInsDiffKind::None && sx > 2 {
|
||||
crossterm::queue!(w, MoveTo(sx - 2, sy))?;
|
||||
let s = match ins_diff.kind {
|
||||
ObjInsDiffKind::Delete => "< ",
|
||||
ObjInsDiffKind::Insert => "> ",
|
||||
_ => "| ",
|
||||
};
|
||||
crossterm::queue!(w, PrintStyledContent(s.with(Color::DarkGrey)))?;
|
||||
} else {
|
||||
crossterm::queue!(w, MoveTo(sx, sy))?;
|
||||
}
|
||||
display_diff(ins_diff, base_addr, |text| -> Result<()> {
|
||||
let mut label_text;
|
||||
let mut base_color = match ins_diff.kind {
|
||||
ObjInsDiffKind::None
|
||||
| ObjInsDiffKind::OpMismatch
|
||||
| ObjInsDiffKind::ArgMismatch => Color::Grey,
|
||||
ObjInsDiffKind::Replace => Color::DarkCyan,
|
||||
ObjInsDiffKind::Delete => Color::DarkRed,
|
||||
ObjInsDiffKind::Insert => Color::DarkGreen,
|
||||
};
|
||||
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::DarkGrey;
|
||||
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::BranchTarget(addr) => {
|
||||
label_text = format!("{addr:x}");
|
||||
}
|
||||
DiffText::Symbol(sym) => {
|
||||
let name = sym.demangled_name.as_ref().unwrap_or(&sym.name);
|
||||
label_text = name.clone();
|
||||
base_color = Color::White;
|
||||
}
|
||||
DiffText::Spacing(n) => {
|
||||
crossterm::queue!(w, MoveRight(n as u16))?;
|
||||
sx += n as u16;
|
||||
return Ok(());
|
||||
}
|
||||
DiffText::Eol => {
|
||||
sy += 1;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
let len = label_text.len();
|
||||
if sx >= max.0 {
|
||||
return Ok(());
|
||||
}
|
||||
let highlighted = *highlight == text;
|
||||
if let Some((cx, cy)) = self.click_xy {
|
||||
if cx >= sx && cx < sx + len as u16 && cy == sy {
|
||||
new_highlight = Some(text.into());
|
||||
}
|
||||
}
|
||||
label_text.truncate(max.0 as usize - sx as usize);
|
||||
let mut content = label_text.with(base_color);
|
||||
if highlighted {
|
||||
content = content.on_dark_grey();
|
||||
}
|
||||
crossterm::queue!(w, PrintStyledContent(content))?;
|
||||
sx += len as u16;
|
||||
if pad_to > len {
|
||||
let pad = (pad_to - len) as u16;
|
||||
crossterm::queue!(w, MoveRight(pad))?;
|
||||
sx += pad;
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
if sy >= max.1 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(new_highlight)
|
||||
}
|
||||
|
||||
fn reload(&mut self) -> Result<()> {
|
||||
let mut target = self
|
||||
.target_path
|
||||
.as_deref()
|
||||
.map(|p| obj::elf::read(p).with_context(|| format!("Loading {}", p.display())))
|
||||
.transpose()?;
|
||||
let mut base = self
|
||||
.base_path
|
||||
.as_deref()
|
||||
.map(|p| obj::elf::read(p).with_context(|| format!("Loading {}", p.display())))
|
||||
.transpose()?;
|
||||
let config = diff::DiffObjConfig::default();
|
||||
diff::diff_objs(&config, target.as_mut(), base.as_mut())?;
|
||||
|
||||
let left_sym = target.as_ref().and_then(|o| find_function(o, &self.symbol_name));
|
||||
let right_sym = base.as_ref().and_then(|o| find_function(o, &self.symbol_name));
|
||||
self.max_len = match (&left_sym, &right_sym) {
|
||||
(Some(l), Some(r)) => l.instructions.len().max(r.instructions.len()),
|
||||
(Some(l), None) => l.instructions.len(),
|
||||
(None, Some(r)) => r.instructions.len(),
|
||||
(None, None) => bail!("Symbol not found: {}", self.symbol_name),
|
||||
};
|
||||
self.left_sym = left_sym;
|
||||
self.right_sym = right_sym;
|
||||
self.reload_time = time::OffsetDateTime::now_local()?;
|
||||
Ok(())
|
||||
}
|
||||
Ok(new_highlight)
|
||||
}
|
||||
|
||||
pub const COLOR_ROTATION: [Color; 8] = [
|
||||
@@ -411,26 +537,3 @@ pub fn match_percent_color(match_percent: f32) -> Color {
|
||||
Color::Red
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub enum HighlightKind {
|
||||
#[default]
|
||||
None,
|
||||
Opcode(u8),
|
||||
Arg(ObjInsArgValue),
|
||||
Symbol(String),
|
||||
Address(u32),
|
||||
}
|
||||
|
||||
impl PartialEq for HighlightKind {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(HighlightKind::None, HighlightKind::None) => false,
|
||||
(HighlightKind::Opcode(a), HighlightKind::Opcode(b)) => a == b,
|
||||
(HighlightKind::Arg(a), HighlightKind::Arg(b)) => a.loose_eq(b),
|
||||
(HighlightKind::Symbol(a), HighlightKind::Symbol(b)) => a == b,
|
||||
(HighlightKind::Address(a), HighlightKind::Address(b)) => a == b,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
fs::File,
|
||||
io::{BufWriter, Write},
|
||||
io::{BufReader, BufWriter, Write},
|
||||
path::{Path, PathBuf},
|
||||
time::Instant,
|
||||
};
|
||||
@@ -16,9 +16,24 @@ use objdiff_core::{
|
||||
use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator};
|
||||
|
||||
#[derive(FromArgs, PartialEq, Debug)]
|
||||
/// Generate a report from a project.
|
||||
/// Commands for processing NVIDIA Shield TV alf files.
|
||||
#[argp(subcommand, name = "report")]
|
||||
pub struct Args {
|
||||
#[argp(subcommand)]
|
||||
command: SubCommand,
|
||||
}
|
||||
|
||||
#[derive(FromArgs, PartialEq, Debug)]
|
||||
#[argp(subcommand)]
|
||||
pub enum SubCommand {
|
||||
Generate(GenerateArgs),
|
||||
Changes(ChangesArgs),
|
||||
}
|
||||
|
||||
#[derive(FromArgs, PartialEq, Debug)]
|
||||
/// Generate a report from a project.
|
||||
#[argp(subcommand, name = "generate")]
|
||||
pub struct GenerateArgs {
|
||||
#[argp(option, short = 'p')]
|
||||
/// Project directory
|
||||
project: Option<PathBuf>,
|
||||
@@ -30,7 +45,22 @@ pub struct Args {
|
||||
deduplicate: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, serde::Serialize)]
|
||||
#[derive(FromArgs, PartialEq, Debug)]
|
||||
/// List any changes from a previous report.
|
||||
#[argp(subcommand, name = "changes")]
|
||||
pub struct ChangesArgs {
|
||||
#[argp(positional)]
|
||||
/// Previous report JSON file
|
||||
previous: PathBuf,
|
||||
#[argp(positional)]
|
||||
/// Current report JSON file
|
||||
current: PathBuf,
|
||||
#[argp(option, short = 'o')]
|
||||
/// Output JSON file
|
||||
output: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
|
||||
struct Report {
|
||||
fuzzy_match_percent: f32,
|
||||
total_size: u64,
|
||||
@@ -42,29 +72,46 @@ struct Report {
|
||||
units: Vec<ReportUnit>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, serde::Serialize)]
|
||||
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
|
||||
struct ReportUnit {
|
||||
name: String,
|
||||
match_percent: f32,
|
||||
fuzzy_match_percent: f32,
|
||||
total_size: u64,
|
||||
matched_size: u64,
|
||||
total_functions: u32,
|
||||
matched_functions: u32,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
complete: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
module_name: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
module_id: Option<u32>,
|
||||
functions: Vec<ReportFunction>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, serde::Serialize)]
|
||||
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
|
||||
struct ReportFunction {
|
||||
name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
demangled_name: Option<String>,
|
||||
#[serde(
|
||||
skip_serializing_if = "Option::is_none",
|
||||
serialize_with = "serialize_hex",
|
||||
deserialize_with = "deserialize_hex"
|
||||
)]
|
||||
address: Option<u64>,
|
||||
size: u64,
|
||||
match_percent: f32,
|
||||
fuzzy_match_percent: f32,
|
||||
}
|
||||
|
||||
pub fn run(args: Args) -> Result<()> {
|
||||
match args.command {
|
||||
SubCommand::Generate(args) => generate(args),
|
||||
SubCommand::Changes(args) => changes(args),
|
||||
}
|
||||
}
|
||||
|
||||
fn generate(args: GenerateArgs) -> Result<()> {
|
||||
let project_dir = args.project.as_deref().unwrap_or_else(|| Path::new("."));
|
||||
log::info!("Loading project {}", project_dir.display());
|
||||
|
||||
@@ -111,7 +158,7 @@ pub fn run(args: Args) -> Result<()> {
|
||||
report.units = units.into_iter().flatten().collect();
|
||||
}
|
||||
for unit in &report.units {
|
||||
report.fuzzy_match_percent += unit.match_percent * unit.total_size as f32;
|
||||
report.fuzzy_match_percent += unit.fuzzy_match_percent * unit.total_size as f32;
|
||||
report.total_size += unit.total_size;
|
||||
report.matched_size += unit.matched_size;
|
||||
report.total_functions += unit.total_functions;
|
||||
@@ -180,7 +227,16 @@ fn report_object(
|
||||
.transpose()?;
|
||||
let config = diff::DiffObjConfig { relax_reloc_diffs: true };
|
||||
diff::diff_objs(&config, target.as_mut(), base.as_mut())?;
|
||||
let mut unit = ReportUnit { name: object.name().to_string(), ..Default::default() };
|
||||
let mut unit = ReportUnit {
|
||||
name: object.name().to_string(),
|
||||
complete: object.complete,
|
||||
module_name: target
|
||||
.as_ref()
|
||||
.and_then(|o| o.split_meta.as_ref())
|
||||
.and_then(|m| m.module_name.clone()),
|
||||
module_id: target.as_ref().and_then(|o| o.split_meta.as_ref()).and_then(|m| m.module_id),
|
||||
..Default::default()
|
||||
};
|
||||
let obj = target.as_ref().or(base.as_ref()).unwrap();
|
||||
for section in &obj.sections {
|
||||
if section.kind != ObjSectionKind::Code {
|
||||
@@ -207,7 +263,7 @@ fn report_object(
|
||||
0.0
|
||||
}
|
||||
});
|
||||
unit.match_percent += match_percent * symbol.size as f32;
|
||||
unit.fuzzy_match_percent += match_percent * symbol.size as f32;
|
||||
unit.total_size += symbol.size;
|
||||
if match_percent == 100.0 {
|
||||
unit.matched_size += symbol.size;
|
||||
@@ -216,7 +272,8 @@ fn report_object(
|
||||
name: symbol.name.clone(),
|
||||
demangled_name: symbol.demangled_name.clone(),
|
||||
size: symbol.size,
|
||||
match_percent,
|
||||
fuzzy_match_percent: match_percent,
|
||||
address: symbol.virtual_address,
|
||||
});
|
||||
if match_percent == 100.0 {
|
||||
unit.matched_functions += 1;
|
||||
@@ -225,9 +282,212 @@ fn report_object(
|
||||
}
|
||||
}
|
||||
if unit.total_size == 0 {
|
||||
unit.match_percent = 100.0;
|
||||
unit.fuzzy_match_percent = 100.0;
|
||||
} else {
|
||||
unit.match_percent /= unit.total_size as f32;
|
||||
unit.fuzzy_match_percent /= unit.total_size as f32;
|
||||
}
|
||||
Ok(Some(unit))
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
|
||||
struct Changes {
|
||||
from: ChangeInfo,
|
||||
to: ChangeInfo,
|
||||
units: Vec<ChangeUnit>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
struct ChangeInfo {
|
||||
fuzzy_match_percent: f32,
|
||||
total_size: u64,
|
||||
matched_size: u64,
|
||||
matched_size_percent: f32,
|
||||
total_functions: u32,
|
||||
matched_functions: u32,
|
||||
matched_functions_percent: f32,
|
||||
}
|
||||
|
||||
impl From<&Report> for ChangeInfo {
|
||||
fn from(report: &Report) -> Self {
|
||||
Self {
|
||||
fuzzy_match_percent: report.fuzzy_match_percent,
|
||||
total_size: report.total_size,
|
||||
matched_size: report.matched_size,
|
||||
matched_size_percent: report.matched_size_percent,
|
||||
total_functions: report.total_functions,
|
||||
matched_functions: report.matched_functions,
|
||||
matched_functions_percent: report.matched_functions_percent,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ReportUnit> for ChangeInfo {
|
||||
fn from(value: &ReportUnit) -> Self {
|
||||
Self {
|
||||
fuzzy_match_percent: value.fuzzy_match_percent,
|
||||
total_size: value.total_size,
|
||||
matched_size: value.matched_size,
|
||||
matched_size_percent: if value.total_size == 0 {
|
||||
100.0
|
||||
} else {
|
||||
value.matched_size as f32 / value.total_size as f32 * 100.0
|
||||
},
|
||||
total_functions: value.total_functions,
|
||||
matched_functions: value.matched_functions,
|
||||
matched_functions_percent: if value.total_functions == 0 {
|
||||
100.0
|
||||
} else {
|
||||
value.matched_functions as f32 / value.total_functions as f32 * 100.0
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
|
||||
struct ChangeUnit {
|
||||
name: String,
|
||||
from: Option<ChangeInfo>,
|
||||
to: Option<ChangeInfo>,
|
||||
functions: Vec<ChangeFunction>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
|
||||
struct ChangeFunction {
|
||||
name: String,
|
||||
from: Option<ChangeFunctionInfo>,
|
||||
to: Option<ChangeFunctionInfo>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
struct ChangeFunctionInfo {
|
||||
fuzzy_match_percent: f32,
|
||||
size: u64,
|
||||
}
|
||||
|
||||
impl From<&ReportFunction> for ChangeFunctionInfo {
|
||||
fn from(value: &ReportFunction) -> Self {
|
||||
Self { fuzzy_match_percent: value.fuzzy_match_percent, size: value.size }
|
||||
}
|
||||
}
|
||||
|
||||
fn changes(args: ChangesArgs) -> Result<()> {
|
||||
let previous = read_report(&args.previous)?;
|
||||
let current = read_report(&args.current)?;
|
||||
let mut changes = Changes {
|
||||
from: ChangeInfo::from(&previous),
|
||||
to: ChangeInfo::from(¤t),
|
||||
units: vec![],
|
||||
};
|
||||
for prev_unit in &previous.units {
|
||||
let prev_unit_info = ChangeInfo::from(prev_unit);
|
||||
let curr_unit = current.units.iter().find(|u| u.name == prev_unit.name);
|
||||
let curr_unit_info = curr_unit.map(ChangeInfo::from);
|
||||
let mut functions = vec![];
|
||||
if let Some(curr_unit) = curr_unit {
|
||||
for prev_func in &prev_unit.functions {
|
||||
let prev_func_info = ChangeFunctionInfo::from(prev_func);
|
||||
let curr_func = curr_unit.functions.iter().find(|f| f.name == prev_func.name);
|
||||
let curr_func_info = curr_func.map(ChangeFunctionInfo::from);
|
||||
if let Some(curr_func_info) = curr_func_info {
|
||||
if prev_func_info != curr_func_info {
|
||||
functions.push(ChangeFunction {
|
||||
name: prev_func.name.clone(),
|
||||
from: Some(prev_func_info),
|
||||
to: Some(curr_func_info),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
functions.push(ChangeFunction {
|
||||
name: prev_func.name.clone(),
|
||||
from: Some(prev_func_info),
|
||||
to: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
for curr_func in &curr_unit.functions {
|
||||
if !prev_unit.functions.iter().any(|f| f.name == curr_func.name) {
|
||||
functions.push(ChangeFunction {
|
||||
name: curr_func.name.clone(),
|
||||
from: None,
|
||||
to: Some(ChangeFunctionInfo::from(curr_func)),
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for prev_func in &prev_unit.functions {
|
||||
functions.push(ChangeFunction {
|
||||
name: prev_func.name.clone(),
|
||||
from: Some(ChangeFunctionInfo::from(prev_func)),
|
||||
to: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
if !functions.is_empty() || !matches!(&curr_unit_info, Some(v) if v == &prev_unit_info) {
|
||||
changes.units.push(ChangeUnit {
|
||||
name: prev_unit.name.clone(),
|
||||
from: Some(prev_unit_info),
|
||||
to: curr_unit_info,
|
||||
functions,
|
||||
});
|
||||
}
|
||||
}
|
||||
for curr_unit in ¤t.units {
|
||||
if !previous.units.iter().any(|u| u.name == curr_unit.name) {
|
||||
changes.units.push(ChangeUnit {
|
||||
name: curr_unit.name.clone(),
|
||||
from: None,
|
||||
to: Some(ChangeInfo::from(curr_unit)),
|
||||
functions: curr_unit
|
||||
.functions
|
||||
.iter()
|
||||
.map(|f| ChangeFunction {
|
||||
name: f.name.clone(),
|
||||
from: None,
|
||||
to: Some(ChangeFunctionInfo::from(f)),
|
||||
})
|
||||
.collect(),
|
||||
});
|
||||
}
|
||||
}
|
||||
if let Some(output) = &args.output {
|
||||
log::info!("Writing to {}", output.display());
|
||||
let mut output = BufWriter::new(
|
||||
File::create(output)
|
||||
.with_context(|| format!("Failed to create file {}", output.display()))?,
|
||||
);
|
||||
serde_json::to_writer_pretty(&mut output, &changes)?;
|
||||
output.flush()?;
|
||||
} else {
|
||||
serde_json::to_writer_pretty(std::io::stdout(), &changes)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_report(path: &Path) -> Result<Report> {
|
||||
serde_json::from_reader(BufReader::new(
|
||||
File::open(path).with_context(|| format!("Failed to open {}", path.display()))?,
|
||||
))
|
||||
.with_context(|| format!("Failed to read report {}", path.display()))
|
||||
}
|
||||
|
||||
fn serialize_hex<S>(x: &Option<u64>, s: S) -> Result<S::Ok, S::Error>
|
||||
where S: serde::Serializer {
|
||||
if let Some(x) = x {
|
||||
s.serialize_str(&format!("{:#x}", x))
|
||||
} else {
|
||||
s.serialize_none()
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_hex<'de, D>(d: D) -> Result<Option<u64>, D::Error>
|
||||
where D: serde::Deserializer<'de> {
|
||||
use serde::Deserialize;
|
||||
let s = String::deserialize(d)?;
|
||||
if s.is_empty() {
|
||||
Ok(None)
|
||||
} else if !s.starts_with("0x") {
|
||||
Err(serde::de::Error::custom("expected hex string"))
|
||||
} else {
|
||||
u64::from_str_radix(&s[2..], 16).map(Some).map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user