diff --git a/Cargo.lock b/Cargo.lock index fe92d77..88bb2f4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -185,9 +185,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.89" +version = "1.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" +checksum = "37bf3594c4c988a53154954629820791dde498571819ae4ca50ca811e060cc95" [[package]] name = "arboard" @@ -712,9 +712,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.30" +version = "1.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b16803a61b81d9eabb7eae2588776c4c1e584b738ede45fdbb4c972cec1e9945" +checksum = "c2e7962b54006dcfcc61cb72735f4d89bb97061dd6a7ed882ec6b8ee53714c6f" dependencies = [ "jobserver", "libc", @@ -1355,7 +1355,7 @@ checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" dependencies = [ "cfg-if", "libc", - "libredox 0.1.3", + "libredox", "windows-sys 0.59.0", ] @@ -1754,9 +1754,9 @@ dependencies = [ [[package]] name = "image" -version = "0.25.2" +version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99314c8a2152b8ddb211f924cdae532d8c5e4c8bb54728e12fff1b0cd5963a10" +checksum = "bc144d44a31d753b02ce64093d532f55ff8dc4ebf2ffb8a63c0dda691385acae" dependencies = [ "bytemuck", "byteorder-lite", @@ -1900,9 +1900,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.159" +version = "0.2.161" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" +checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" [[package]] name = "libloading" @@ -1914,17 +1914,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "libredox" -version = "0.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3af92c55d7d839293953fcd0fda5ecfe93297cfde6ffbdec13b41d99c0ba6607" -dependencies = [ - "bitflags 2.6.0", - "libc", - "redox_syscall 0.4.1", -] - [[package]] name = "libredox" version = "0.1.3" @@ -2413,11 +2402,11 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "orbclient" -version = "0.3.47" +version = "0.3.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52f0d54bde9774d3a51dcf281a5def240c71996bc6ca05d2c847ec8b2b216166" +checksum = "ba0b26cec2e24f08ed8bb31519a9333140a6599b867dac464bb150bdb796fd43" dependencies = [ - "libredox 0.0.2", + "libredox", ] [[package]] @@ -2706,7 +2695,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ "getrandom", - "libredox 0.1.3", + "libredox", "thiserror", ] @@ -2875,9 +2864,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.128" +version = "1.0.132" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" dependencies = [ "itoa", "memchr", diff --git a/README.md b/README.md index 6c0303f..e974226 100644 --- a/README.md +++ b/README.md @@ -7,33 +7,39 @@ A(nother) binary diff tool, targeted toward decompilation and modding projects. ![image of bdiff UI](screenshot.png) ## Features + - Automatic reload of opened files on change - Pairwise byte diff display (vbindiff style) - String, data viewer for various formats and encodings -- Support for displaying symbol information from binaries by parsing .map files via [mapfile_parser](https://github.com/Decompollaborate/mapfile_parser) +- Support for displaying symbol information from binaries by parsing .map files + via [mapfile_parser](https://github.com/Decompollaborate/mapfile_parser) -bdiff is currently in the very early stages of development. See the [issues](https://github.com/ethteck/bdiff/issues) for planned features. +bdiff is currently in the very early stages of development. See the [issues](https://github.com/ethteck/bdiff/issues) +for planned features. ## Why? -There's a million other hex viewers out there. Most people in the game decompilation scene use vbindiff, a very dependable but somewhat feature-sparse tool. Over the years, I've started wishing for little things here and there that I wish it could do, and I've also been looking to learn Rust. +There's a million other hex viewers out there. Most people in the game decompilation scene use vbindiff, a very +dependable but somewhat feature-sparse tool. Over the years, I've started wishing for little things here and there that +I wish it could do, and I've also been looking to learn Rust. ## Configuration -To provide a more convenient experience, projects can specify a "bdiff.json" configuration file which defines a startup configuration for the program. An example config follows: +To provide a more convenient experience, projects can specify a "bdiff.json" configuration file which defines a +workspace configuration for the program. An example config follows: -``` +```yaml { - "files": [ - { - "path": "C:\\somethin.z64", - "map": "C:\\somethin.map" - }, - { - "path": "C:\\another.z64", - "map": "C:\\another.map" - } - ] + "files": [ + { + "path": "C:\\somethin.z64", + "map": "C:\\somethin.map", + }, + { + "path": "C:\\another.z64", + "map": "C:\\another.map", + } + ] } ``` @@ -42,4 +48,5 @@ So far, the configuration format simply consists of a list of files to open (`fi For each file, there are two fields: * `path`: The path to the file -* `map` (optional): The path to a GNU ld or Clang lld .map file, to be parsed so symbol information is displayed in the viewer \ No newline at end of file +* `map` (optional): The path to a GNU ld or Clang lld .map file, to be parsed so symbol information is displayed in the + viewer diff --git a/app/Cargo.toml b/app/Cargo.toml index 9bd84b5..6795ef7 100644 --- a/app/Cargo.toml +++ b/app/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" authors = ["Ethan Roseman "] license = "MIT" repository = "https://github.com/ethteck/bdiff" -readme = "README.md" +readme = "../README.md" description = """ A(nother) binary diffing tool """ @@ -21,7 +21,7 @@ eframe = { version = "0.29.1", features = ["persistence"] } egui-modal = "0.5.0" egui-phosphor = "0.7.3" encoding_rs = "0.8.34" -bdiff_hex_view = { version = "0.9.0", path="../hex_view" } +bdiff_hex_view = { version = "0.9.0", path = "../hex_view" } iset = "0.3.0" log = "0.4.22" mapfile_parser = "2.7.1" diff --git a/app/src/app.rs b/app/src/app.rs index 73c2127..091fa5c 100644 --- a/app/src/app.rs +++ b/app/src/app.rs @@ -12,13 +12,14 @@ use crate::{ workspace::{read_workspace_json, write_workspace_json, Workspace, WorkspaceFile}, }; use anyhow::Error; -use bdiff_hex_view::{CursorState, HexViewSelection, HexViewSelectionSide, HexViewSelectionState}; +use bdiff_hex_view::cursor_state::CursorState; +use bdiff_hex_view::selection::{HexViewSelection, HexViewSelectionSide, HexViewSelectionState}; use eframe::egui::{Align, Layout, Modifiers, RichText, Ui}; use eframe::{ egui::{self, Checkbox, Context, Style, ViewportCommand}, epaint::{Rounding, Shadow}, }; -use egui_modal::Modal; +use egui_modal::{Modal, ModalStyle}; #[derive(Default)] struct GotoModal { @@ -98,14 +99,12 @@ impl BdiffApp { Workspace::default() }; - for file in config.files.iter() { match ret.open_file(&file.path) { Ok(fv) => { if let Some(map) = file.map.as_ref() { fv.st.load_file(map); } - fv.file.endianness = file.endianness; // TODO hook up endianness saving fv.hv.set_style(hv_style.clone()); } Err(e) => { @@ -160,9 +159,7 @@ impl BdiffApp { && fv.hv.selection.start() >= bytes_per_row && fv.hv.selection.end() >= bytes_per_row { - fv.hv - .selection - .adjust_cur_pos(-(bytes_per_row as isize)); + fv.hv.selection.adjust_cur_pos(-(bytes_per_row as isize)); changed = true; } if ctx.input(|i| i.key_pressed(egui::Key::ArrowDown)) @@ -212,7 +209,12 @@ impl BdiffApp { } // Keep positions zeroed - let lowest_fv_pos = self.file_views.iter_mut().map(|fv| fv.cur_pos).min().unwrap(); + let lowest_fv_pos = self + .file_views + .iter_mut() + .map(|fv| fv.cur_pos) + .min() + .unwrap(); if lowest_fv_pos > 0 { for fv in self.file_views.iter_mut() { fv.cur_pos -= lowest_fv_pos; @@ -232,11 +234,11 @@ impl BdiffApp { self.global_view_pos = 0.max(self.global_view_pos as isize + delta) as usize; } - fn move_global_pos_enter(&mut self, longest_file_len: usize, bytes_per_screen: usize) { + fn move_global_pos_enter(&mut self, furthest_file_pos: usize, bytes_per_screen: usize) { if self.is_diffing() { let last_byte = self.global_view_pos + bytes_per_screen; - if last_byte < longest_file_len { + if last_byte < furthest_file_pos { match self.diff_state.get_next_diff(last_byte) { Some(next_diff) => { // Move to the next diff @@ -244,8 +246,8 @@ impl BdiffApp { self.set_global_pos(new_pos); } None => { - // Move to the end of the file - self.set_global_pos(0.max(longest_file_len - bytes_per_screen)) + // Move to the end of the diff + self.set_global_pos(0.max(furthest_file_pos - bytes_per_screen)) } } } @@ -259,8 +261,41 @@ impl BdiffApp { self.file_views.len() > 1 && self.settings.diff_enabled } + fn scroll_view(&mut self, ctx: &Context) { + let scroll_y = ctx.input(|i| i.raw_scroll_delta.y); + + // Scrolling + if scroll_y != 0.0 { + let lines_per_scroll = 1; + let scroll_threshold = 20; // One tick of the scroll wheel for me + let scroll_amt: isize; + + if scroll_y.abs() >= scroll_threshold as f32 { + // Scroll wheels / very fast scrolling + scroll_amt = scroll_y as isize / scroll_threshold; + self.scroll_overflow = 0.0; + } else { + // Trackpads - Accumulate scroll amount until it reaches the threshold + self.scroll_overflow += scroll_y; + scroll_amt = self.scroll_overflow as isize / scroll_threshold; + if scroll_amt != 0 { + self.scroll_overflow -= (scroll_amt * scroll_threshold) as f32; + } + } + self.move_global_pos(-scroll_amt * lines_per_scroll * self.bytes_per_row as isize); + } + } + + fn get_furthest_file_pos(&self) -> usize { + self.file_views + .iter() + .map(|fv| fv.cur_pos + fv.file.data.len()) + .max() + .unwrap() + } + fn move_view(&mut self, ctx: &Context) { - let longest_file_len = self.file_views.iter().map(|fv| fv.file.data.len()).max().unwrap(); + let furthest_file_pos = self.get_furthest_file_pos(); let bytes_per_screen = self.bytes_per_row * self.num_rows; // Keys @@ -268,7 +303,7 @@ impl BdiffApp { self.set_global_pos(0); } if ctx.input(|i| i.key_pressed(egui::Key::End)) { - self.set_global_pos(0.max(longest_file_len - bytes_per_screen)) + self.set_global_pos(0.max(furthest_file_pos - bytes_per_screen)) } if ctx.input(|i| i.key_pressed(egui::Key::PageUp)) { self.move_global_pos(-(bytes_per_screen as isize)) @@ -289,30 +324,7 @@ impl BdiffApp { self.move_global_pos(self.bytes_per_row as isize); } if ctx.input(|i| i.key_pressed(egui::Key::Enter)) { - self.move_global_pos_enter(longest_file_len, bytes_per_screen); - } - - let scroll_y = ctx.input(|i| i.raw_scroll_delta.y); - - // Scrolling - if scroll_y != 0.0 { - let lines_per_scroll = 1; - let scroll_threshold = 20; // One tick of the scroll wheel for me - let scroll_amt: isize; - - if scroll_y.abs() >= scroll_threshold as f32 { - // Scroll wheels / very fast scrolling - scroll_amt = scroll_y as isize / scroll_threshold; - self.scroll_overflow = 0.0; - } else { - // Trackpads - Accumulate scroll amount until it reaches the threshold - self.scroll_overflow += scroll_y; - scroll_amt = self.scroll_overflow as isize / scroll_threshold; - if scroll_amt != 0 { - self.scroll_overflow -= (scroll_amt * scroll_threshold) as f32; - } - } - self.move_global_pos(-scroll_amt * lines_per_scroll * self.bytes_per_row as isize); + self.move_global_pos_enter(furthest_file_pos, bytes_per_screen); } } @@ -324,11 +336,43 @@ impl BdiffApp { if ctx.input(|i| i.modifiers.shift) { // Move selection self.move_selection(ctx); - } else if self.file_views.iter().any(|fv| fv.pos_locked) { - self.nudge_files(ctx); } else { - // Move view - self.move_view(ctx); + if self.file_views.iter().any(|fv| fv.pos_locked) { + self.nudge_files(ctx); + } else { + self.move_view(ctx); + } + + // Scroll wheel + self.scroll_view(ctx); + } + } + + fn copy_selected_bytes(&self, ctx: &Context) { + let mut selection = String::new(); + + for fv in self.file_views.iter() { + if self.last_selected_hv.is_some() && fv.id == self.last_selected_hv.unwrap() { + let selected_bytes = fv.hv.get_selected_bytes(&fv.file.data, fv.cur_pos); + + let selected_bytes: String = match fv.hv.selection.side { + HexViewSelectionSide::Hex => selected_bytes + .iter() + .map(|b| format!("{:02X}", b)) + .collect::>() + .join(" "), + HexViewSelectionSide::Ascii => { + String::from_utf8_lossy(selected_bytes).to_string() + } + }; + // convert selected_bytes to an ascii string + + selection.push_str(&selected_bytes.to_string()); + } + } + + if !selection.is_empty() { + ctx.output_mut(|o| o.copied_text = selection); } } } @@ -385,30 +429,21 @@ impl eframe::App for BdiffApp { style.interaction.multi_widget_text_select = false; ctx.set_style(style); - // Consume tab keypresses so they don't cause egui to switch focus - ctx.input_mut(|i| i.consume_key(Modifiers::NONE, egui::Key::Tab)); + let cursor_state = CursorState::get(ctx); - let cursor_state: CursorState = ctx.input(|i| { - if i.pointer.primary_pressed() { - CursorState::Pressed - } else if i.pointer.primary_down() { - CursorState::StillDown - } else if i.pointer.primary_released() { - CursorState::Released - } else { - CursorState::Hovering - } - }); + let modal_style = ModalStyle { + overlay_color: egui::Color32::from_black_alpha(100), + ..Default::default() + }; - let goto_modal: Modal = Modal::new(ctx, "goto_modal"); + let goto_modal: Modal = Modal::new(ctx, "goto_modal").with_style(&modal_style); + let overwrite_modal: Modal = Modal::new(ctx, "overwrite_modal").with_style(&modal_style); // Goto modal goto_modal.show(|ui| { self.show_goto_modal(&goto_modal, ui, ctx); }); - let overwrite_modal: Modal = Modal::new(ctx, "overwrite_modal"); - if self.overwrite_modal.open { self.show_overwrite_modal(&overwrite_modal); overwrite_modal.open(); @@ -448,35 +483,6 @@ impl eframe::App for BdiffApp { } } - // Copy selection - if ctx.input(|i| i.modifiers.command && i.key_pressed(egui::Key::C)) { - let mut selection = String::new(); - - for fv in self.file_views.iter() { - if self.last_selected_hv.is_some() && fv.id == self.last_selected_hv.unwrap() { - let selected_bytes = fv.hv.get_selected_bytes(&fv.file.data); - - let selected_bytes: String = match fv.hv.selection.side { - HexViewSelectionSide::Hex => selected_bytes - .iter() - .map(|b| format!("{:02X}", b)) - .collect::>() - .join(" "), - HexViewSelectionSide::Ascii => { - String::from_utf8_lossy(selected_bytes).to_string() - } - }; - // convert selected_bytes to an ascii string - - selection.push_str(&selected_bytes.to_string()); - } - } - - if !selection.is_empty() { - ctx.output_mut(|o| o.copied_text = selection); - } - } - // Menu bar egui::TopBottomPanel::top("top_panel").show(ctx, |ui| { egui::menu::bar(ui, |ui| { @@ -515,15 +521,20 @@ impl eframe::App for BdiffApp { }; if ui.button(enter_text).clicked() && self.file_views.len() > 1 { - let longest_file_len = self.file_views.iter().map(|fv| fv.file.data.len()).max().unwrap(); - let bytes_per_screen = self.bytes_per_row * self.num_rows; + self.move_global_pos_enter( + self.get_furthest_file_pos(), + self.bytes_per_row * self.num_rows, + ); + } - self.move_global_pos_enter(longest_file_len, bytes_per_screen); + if ui.button("Copy selected bytes/hex").clicked() { + self.copy_selected_bytes(ctx); } }); ui.menu_button("Options", |ui| { - let diff_checkbox = Checkbox::new(&mut self.settings.diff_enabled, "Display diff"); + let diff_checkbox = + Checkbox::new(&mut self.settings.diff_enabled, "Display diff"); let mirror_selection_checkbox = Checkbox::new( &mut self.settings.mirror_selection, "Mirror selection across files", @@ -590,17 +601,17 @@ impl eframe::App for BdiffApp { ctx, &self.settings, &self.diff_state, - cursor_state, can_selection_change, self.global_view_pos, - self.bytes_per_row, - self.num_rows, ); if fv.closed { // Remove file from the workspace if it's closed. - if let Some(pos) = - self.workspace.files.iter().position(|a| a.path == fv.file.path) + if let Some(pos) = self + .workspace + .files + .iter() + .position(|a| a.path == fv.file.path) { self.workspace.files.remove(pos); } @@ -639,11 +650,6 @@ impl eframe::App for BdiffApp { for fv in self.file_views.iter_mut() { if fv.hv.selection != self.global_selection { fv.hv.selection = self.global_selection.clone(); - if fv.hv.selection.start() >= fv.file.data.len() - || fv.hv.selection.end() >= fv.file.data.len() - { - fv.hv.selection.clear() - } } } } diff --git a/app/src/bin_file.rs b/app/src/bin_file.rs index 620d03a..6df3634 100644 --- a/app/src/bin_file.rs +++ b/app/src/bin_file.rs @@ -10,6 +10,7 @@ use anyhow::Error; use serde::{Deserialize, Serialize}; #[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] pub enum Endianness { Little, #[default] @@ -58,7 +59,7 @@ impl BinFile { ..Default::default() }; - match create_watcher(path, ret.modified.clone()).map_err(anyhow::Error::new) { + match create_watcher(path, ret.modified.clone()).map_err(Error::new) { Ok(watcher) => { ret.watcher = Some(watcher); } diff --git a/app/src/file_view.rs b/app/src/file_view.rs index d71a904..485f31d 100644 --- a/app/src/file_view.rs +++ b/app/src/file_view.rs @@ -1,10 +1,3 @@ -use anyhow::Error; -use bdiff_hex_view::{CursorState, HexView, HexViewSelectionState}; -use eframe::{ - egui::{self, Id}, - epaint::Color32, -}; - use crate::tools::data_viewer::DataViewer; use crate::tools::string_viewer::StringViewer; use crate::{ @@ -13,6 +6,14 @@ use crate::{ settings::Settings, tools::symbol_tool::SymbolTool, }; +use anyhow::Error; +use bdiff_hex_view::cursor_state::CursorState; +use bdiff_hex_view::selection::HexViewSelectionState; +use bdiff_hex_view::{HexView, HexViewOptions, HexViewState}; +use eframe::{ + egui::{self, Id}, + epaint::Color32, +}; pub struct FileView { pub id: usize, @@ -48,8 +49,8 @@ impl FileView { pub fn reload_file(&mut self) -> Result<(), Error> { self.file.data = read_file_bytes(self.file.path.clone())?; - if self.hv.selection.range.first >= self.file.data.len() - && self.hv.selection.range.second >= self.file.data.len() + if self.hv.selection.start() >= self.file.data.len() + && self.hv.selection.end() >= self.file.data.len() { self.hv.selection.clear(); } else { @@ -61,36 +62,13 @@ impl FileView { Ok(()) } - pub fn get_display_bytes(&self, global_offset: usize, num_bytes: usize) -> Vec> { - let pos: isize = global_offset as isize - self.cur_pos as isize; - - if pos > 0 && (pos as usize) > self.file.data.len() { - vec![None; num_bytes] - } else { - let mut bytes = Vec::with_capacity(num_bytes); - for i in 0..num_bytes { - let idx = pos + i as isize; - if idx >= 0 && (idx as usize) < self.file.data.len() { - bytes.push(Some(self.file.data[idx as usize])); - } else { - bytes.push(None); - } - } - bytes - } - } - - pub fn show( &mut self, ctx: &egui::Context, settings: &Settings, diff_state: &DiffState, - cursor_state: CursorState, can_selection_change: bool, global_view_pos: usize, - bytes_per_row: usize, - num_rows: usize, ) { egui::Window::new(self.file.path.to_str().unwrap()) .id(Id::new(format!("hex_view_window_{}", self.id))) @@ -113,18 +91,18 @@ impl FileView { .size(14.0) .color(Color32::LIGHT_GRAY), ) - .on_hover_text(egui::RichText::new(file_name)); + .on_hover_text(egui::RichText::new(file_name)); let (lock_text, hover_text) = match self.pos_locked { true => ( egui::RichText::new(egui_phosphor::regular::LOCK_SIMPLE) .color(Color32::RED), - "Unlock scroll position", + "Unlock file position", ), false => ( egui::RichText::new(egui_phosphor::regular::LOCK_SIMPLE_OPEN) .color(Color32::GREEN), - "Lock scroll position", + "Lock file position", ), }; if ui.button(lock_text).on_hover_text(hover_text).clicked() { @@ -173,20 +151,30 @@ impl FileView { ui.group(|ui| { let diffs = match settings.diff_enabled { true => Some(&diff_state.diffs[..]), - false => None + false => None, }; - let display_data = self.get_display_bytes(global_view_pos, bytes_per_row * num_rows); + let num_offset_digits = match self.file.data.len() { + //0..=0xFFFF => 4, + 0x10000..=0xFFFFFFFF => 8, + 0x100000000..=0xFFFFFFFFFFFF => 12, + _ => 8, + }; self.hv.show( ui, - &display_data, - &diffs, - global_view_pos, - self.cur_pos, - cursor_state, - can_selection_change, - settings.byte_grouping, + &HexViewState { + file_data: &self.file.data, + file_pos: self.cur_pos, + global_pos: global_view_pos, + diffs, + }, + CursorState::get(ctx), + HexViewOptions { + can_selection_change, + byte_grouping: settings.byte_grouping, + num_offset_digits, + }, ); }); @@ -194,13 +182,21 @@ impl FileView { let selection_text = match self.hv.selection.state { HexViewSelectionState::None => "No selection".to_owned(), _ => { - let start = self.hv.selection.start(); - let end = self.hv.selection.end(); + // Convert to file coords + let start = self.hv.selection.start() as isize + - self.cur_pos as isize; + let end = self.hv.selection.end() as isize + - self.cur_pos as isize; let length = end - start + 1; let map_entry = match self.st.map_file { Some(ref map_file) => { - map_file.get_entry(start, end + 1) + if start >= 0 && end >= 0 { + map_file + .get_entry(start as usize, end as usize + 1) + } else { + None + } } None => None, }; @@ -210,9 +206,17 @@ impl FileView { format!("Selection: 0x{:X}", start) } _ => { + let start_str = match start < 0 { + true => format!("-0x{:X}", -start), + false => format!("0x{:X}", start), + }; + let end_str = match end < 0 { + true => format!("-0x{:X}", -end), + false => format!("0x{:X}", end), + }; format!( - "Selection: 0x{:X} - 0x{:X} (len 0x{:X})", - start, end, length + "Selection: {} - {} (len 0x{:X})", + start_str, end_str, length ) } }; @@ -223,7 +227,7 @@ impl FileView { "{} ({} + 0x{})", beginning, entry.symbol_name, - start - entry.symbol_vrom + start as usize - entry.symbol_vrom ) } None => beginning, @@ -236,21 +240,31 @@ impl FileView { if self.show_cursor_info { let hover_text = match self.hv.cursor_pos { Some(pos) => { - let map_entry = match self.st.map_file { - Some(ref map_file) => map_file.get_entry(pos, pos + 1), - None => None, - }; + if pos < self.cur_pos { + "Not hovering".to_owned() + } else { + // Convert to file position from global position + let pos = + (pos as isize - self.cur_pos as isize) as usize; - match map_entry { - Some(entry) => { - format!( - "Cursor: 0x{:X} ({} + 0x{})", - pos, - entry.symbol_name, - pos - entry.symbol_vrom - ) + let map_entry = match self.st.map_file { + Some(ref map_file) => { + map_file.get_entry(pos, pos + 1) + } + None => None, + }; + + match map_entry { + Some(entry) => { + format!( + "Cursor: 0x{:X} ({} + 0x{})", + pos, + entry.symbol_name, + pos - entry.symbol_vrom + ) + } + None => format!("Cursor: 0x{:X}", pos), } - None => format!("Cursor: 0x{:X}", pos), } } None => "Not hovering".to_owned(), @@ -263,13 +277,13 @@ impl FileView { self.dv.display( ui, self.id, - self.hv.get_selected_bytes(&self.file.data), + self.hv.get_selected_bytes(&self.file.data, self.cur_pos), self.file.endianness, ); self.sv.display( ui, self.id, - self.hv.get_selected_bytes(&self.file.data), + self.hv.get_selected_bytes(&self.file.data, self.cur_pos), self.file.endianness, ); self.st.display(ui); diff --git a/app/src/main.rs b/app/src/main.rs index a19fb62..209fa63 100644 --- a/app/src/main.rs +++ b/app/src/main.rs @@ -1,16 +1,13 @@ -#![cfg_attr( - not(debug_assertions), - windows_subsystem = "windows" -)] // hide console window on Windows in release +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release mod app; mod bin_file; -mod workspace; mod diff_state; mod file_view; mod settings; -mod watcher; mod tools; +mod watcher; +mod workspace; use std::path::PathBuf; diff --git a/app/src/settings/mod.rs b/app/src/settings/mod.rs index 70920f4..190fda2 100644 --- a/app/src/settings/mod.rs +++ b/app/src/settings/mod.rs @@ -8,8 +8,8 @@ use std::{ path::PathBuf, }; -pub mod ui; pub mod theme; +pub mod ui; pub use theme::show_theme_settings; diff --git a/app/src/settings/theme.rs b/app/src/settings/theme.rs index 9d3e715..b328d16 100644 --- a/app/src/settings/theme.rs +++ b/app/src/settings/theme.rs @@ -20,7 +20,7 @@ impl Display for VisualTheme { Self::Dark => "Dark", Self::Light => "Light", } - .to_string(); + .to_string(); write!(f, "{}", str) } } @@ -65,9 +65,7 @@ pub fn show_theme_settings(ctx: &egui::Context, settings: &mut ThemeSettings) -> color_selection( ui, "Leading zero color", - &mut settings - .hex_view_style - .offset_leading_zero_color, + &mut settings.hex_view_style.offset_leading_zero_color, ); }); @@ -79,11 +77,7 @@ pub fn show_theme_settings(ctx: &egui::Context, settings: &mut ThemeSettings) -> "Selection color", &mut settings.hex_view_style.selection_color, ); - color_selection( - ui, - "Diff color", - &mut settings.hex_view_style.diff_color, - ); + color_selection(ui, "Diff color", &mut settings.hex_view_style.diff_color); color_selection( ui, "Null color", diff --git a/app/src/settings/ui.rs b/app/src/settings/ui.rs index bcc6bd0..786561e 100644 --- a/app/src/settings/ui.rs +++ b/app/src/settings/ui.rs @@ -29,10 +29,13 @@ pub fn show_settings_management_buttons(ui: &mut Ui, settings: &mut impl Setting } ui.separator(); - if ui.button(RichText::new(format!( - "{} Save", - egui_phosphor::regular::FLOPPY_DISK - ))).clicked() { + if ui + .button(RichText::new(format!( + "{} Save", + egui_phosphor::regular::FLOPPY_DISK + ))) + .clicked() + { settings.save(); } }); diff --git a/app/src/tools/mod.rs b/app/src/tools/mod.rs index 2e8f9a9..47556ee 100644 --- a/app/src/tools/mod.rs +++ b/app/src/tools/mod.rs @@ -1,5 +1,5 @@ -pub(crate) mod symbol_tool; -pub mod string_viewer; pub mod data_viewer; +pub mod string_viewer; +pub(crate) mod symbol_tool; -mod map_file; \ No newline at end of file +mod map_file; diff --git a/app/src/workspace.rs b/app/src/workspace.rs index 0adfa9f..81cee45 100644 --- a/app/src/workspace.rs +++ b/app/src/workspace.rs @@ -4,7 +4,6 @@ use std::{ path::{Path, PathBuf}, }; -use crate::bin_file::Endianness; use anyhow::{Context, Error}; use serde::{Deserialize, Serialize}; @@ -12,19 +11,18 @@ use serde::{Deserialize, Serialize}; pub struct WorkspaceFile { pub path: PathBuf, pub map: Option, - pub endianness: Endianness, } impl From for WorkspaceFile { fn from(path: PathBuf) -> Self { - Self { path, map: None, endianness: Endianness::Big } + Self { path, map: None } } } impl From<&Path> for WorkspaceFile { fn from(path: &Path) -> Self { let path: PathBuf = path.into(); - Self { path, map: None, endianness: Endianness::Big } + Self { path, map: None } } } diff --git a/hex_view/src/cursor_state.rs b/hex_view/src/cursor_state.rs new file mode 100644 index 0000000..13cd868 --- /dev/null +++ b/hex_view/src/cursor_state.rs @@ -0,0 +1,25 @@ +use egui::Context; + +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum CursorState { + Hovering, + Pressed, + StillDown, + Released, +} + +impl CursorState { + pub fn get(ctx: &Context) -> Self { + ctx.input(|i| { + if i.pointer.primary_pressed() { + Self::Pressed + } else if i.pointer.primary_down() { + Self::StillDown + } else if i.pointer.primary_released() { + Self::Released + } else { + Self::Hovering + } + }) + } +} \ No newline at end of file diff --git a/hex_view/src/hex_view.rs b/hex_view/src/hex_view.rs index 1aea576..2bff0b2 100644 --- a/hex_view/src/hex_view.rs +++ b/hex_view/src/hex_view.rs @@ -1,88 +1,10 @@ -use crate::{spacer::Spacer, theme::HexViewStyle}; +use crate::{spacer::Spacer, theme::HexViewStyle, Color}; use crate::byte_grouping::ByteGrouping; +use crate::cursor_state::CursorState; +use crate::selection::{HexViewSelection, HexViewSelectionSide, HexViewSelectionState}; use egui::{self, Color32, FontId, Sense, Separator}; -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum CursorState { - Hovering, - Pressed, - StillDown, - Released, -} - -#[derive(Clone, Debug, Default, PartialEq)] -pub struct HexViewSelectionRange { - pub first: usize, - pub second: usize, -} - -#[derive(Clone, Default, Debug, PartialEq)] -pub enum HexViewSelectionState { - #[default] - None, - Selecting, - Selected, -} - -#[derive(Clone, Default, Debug, PartialEq)] -pub enum HexViewSelectionSide { - #[default] - Hex, - Ascii, -} - -#[derive(Clone, Debug, Default, PartialEq)] -pub struct HexViewSelection { - pub range: HexViewSelectionRange, - pub state: HexViewSelectionState, - pub side: HexViewSelectionSide, -} - -impl HexViewSelection { - pub fn start(&self) -> usize { - self.range.first.min(self.range.second) - } - - pub fn end(&self) -> usize { - self.range.second.max(self.range.first) - } - - fn contains(&self, grid_pos: usize) -> bool { - self.state != HexViewSelectionState::None - && grid_pos >= self.start() - && grid_pos <= self.end() - } - - pub fn begin(&mut self, grid_pos: usize, side: HexViewSelectionSide) { - self.range.first = grid_pos; - self.range.second = grid_pos; - self.state = HexViewSelectionState::Selecting; - self.side = side; - } - - pub fn update(&mut self, grid_pos: usize) { - self.range.second = grid_pos; - } - - pub fn finalize(&mut self, grid_pos: usize) { - self.range.second = grid_pos; - self.state = HexViewSelectionState::Selected; - } - - pub fn clear(&mut self) { - self.range.first = 0; - self.range.second = 0; - self.state = HexViewSelectionState::None; - self.side = HexViewSelectionSide::default(); - } - - pub fn adjust_cur_pos(&mut self, delta: isize) { - self.range.first = (self.range.first as isize + delta).max(0) as usize; - self.range.second = (self.range.second as isize + delta).max(0) as usize; - } -} - #[derive(Clone, Default, PartialEq)] pub struct HexView { pub id: usize, @@ -93,6 +15,19 @@ pub struct HexView { pub cursor_pos: Option, } +pub struct HexViewOptions { + pub can_selection_change: bool, + pub byte_grouping: ByteGrouping, + pub num_offset_digits: usize, +} + +pub struct HexViewState<'state> { + pub file_data: &'state [u8], + pub file_pos: usize, + pub global_pos: usize, + pub diffs: Option<&'state [bool]>, +} + impl HexView { pub fn new(id: usize, bytes_per_row: usize, num_rows: usize) -> Self { Self { @@ -109,12 +44,12 @@ impl HexView { self.style = style; } - pub fn get_selected_bytes<'data>(&self, data: &'data [u8]) -> &'data [u8] { + pub fn get_selected_bytes<'data>(&self, data: &'data [u8], file_pos: usize) -> &'data [u8] { match self.selection.state { HexViewSelectionState::None => &[], HexViewSelectionState::Selecting | HexViewSelectionState::Selected => { - let start = self.selection.start(); - let end = self.selection.end(); + let start = (self.selection.start() as isize - file_pos as isize).max(0) as usize; + let end = (self.selection.end() as isize - file_pos as isize).max(0) as usize; if start < data.len() { &data[start..(end + 1).min(data.len())] } else { @@ -124,7 +59,9 @@ impl HexView { } } - fn show_offset(&mut self, num_digits: i32, current_pos: isize, ui: &mut egui::Ui) { + fn show_offset(&mut self, num_digits: usize, current_pos: isize, ui: &mut egui::Ui) { + let num_digits: i32 = num_digits as i32; + let mut i: i32 = num_digits; let mut offset_leading_zeros = true; @@ -161,28 +98,35 @@ impl HexView { } } + fn get_selection_color(&self, pos: usize) -> Color { + if self.selection.contains(pos) { + self.style.selection_color.clone() + } else { + Color32::TRANSPARENT.into() + } + } + fn show_hex( &mut self, ui: &mut egui::Ui, - byte_grouping: ByteGrouping, - start_pos: isize, - start_diff_pos: usize, - row: &[Option], - diffs: &Option<&[bool]>, - can_selection_change: bool, + row: usize, + row_data: &[Option], + state: &HexViewState, cursor_state: CursorState, + options: &HexViewOptions, ) { let mut i = 0; + let mut cur_pos = state.file_pos + row * self.bytes_per_row; + let mut global_pos = state.global_pos + row * self.bytes_per_row; + while i < self.bytes_per_row { - let byte_grouping: usize = byte_grouping.into(); + let byte_grouping: usize = options.byte_grouping.into(); if i > 0 && (i % byte_grouping) == 0 { ui.add(Spacer::default().spacing_x(4.0)); } - let pos = start_pos + i as isize; - let diff_pos = start_diff_pos + i; - let byte: Option = row[i]; + let byte: Option = row_data[i]; let byte_text = match byte { Some(byte) => format!("{:02X}", byte), @@ -193,9 +137,10 @@ impl HexView { egui::RichText::new(byte_text) .font(FontId::monospace(self.style.font_size)) .color( - if diffs.is_some_and(|diffs| { - diff_pos < diffs.len() && diffs[diff_pos] - }) { + if state + .diffs + .is_some_and(|diffs| global_pos < diffs.len() && diffs[global_pos]) + { self.style.diff_color.clone() } else { match byte { @@ -204,33 +149,29 @@ impl HexView { } }, ) - .background_color({ - if pos >= 0 && self.selection.contains(pos as usize) { - self.style.selection_color.clone() - } else { - Color32::TRANSPARENT.into() - } - }), + .background_color(self.get_selection_color(global_pos)), ) - .sense(Sense::click_and_drag()); + .sense(Sense::click_and_drag()); let res = ui.add(hex_label); if byte.is_some() { if res.contains_pointer() { - self.cursor_pos = Some(pos as usize); + self.cursor_pos = Some(cur_pos); } - if can_selection_change { + if options.can_selection_change { self.handle_selection( ui, res, cursor_state, - pos as usize, + global_pos, HexViewSelectionSide::Hex, ); } } i += 1; + cur_pos += 1; + global_pos += 1; if i < self.bytes_per_row { ui.add(Spacer::default().spacing_x(4.0)); @@ -240,17 +181,19 @@ impl HexView { fn show_ascii( &mut self, - row: &[Option], - current_pos: isize, ui: &mut egui::Ui, - can_selection_change: bool, + row: usize, + row_data: &[Option], + state: &HexViewState, cursor_state: CursorState, + options: &HexViewOptions, ) { let mut i = 0; - while i < self.bytes_per_row { - let byte: Option = row[i]; + let mut cur_pos = state.file_pos + row * self.bytes_per_row; + let mut global_pos = state.global_pos + row * self.bytes_per_row; - let row_current_pos = current_pos + i as isize; + while i < self.bytes_per_row { + let byte: Option = row_data[i]; let ascii_char = match byte { Some(32..=126) => byte.unwrap() as char, @@ -266,89 +209,93 @@ impl HexView { Some(32..=126) => self.style.ascii_color.clone(), _ => self.style.other_ascii_color.clone(), }) - .background_color({ - if row_current_pos >= 0 && self.selection.contains(row_current_pos as usize) { - self.style.selection_color.clone() - } else { - Color32::TRANSPARENT.into() - } - }), + .background_color(self.get_selection_color(global_pos)), ) - .sense(Sense::click_and_drag()); + .sense(Sense::click_and_drag()); let res = ui.add(hex_label); ui.add(Spacer::default().spacing_x(1.0)); if byte.is_some() { if res.contains_pointer() { - self.cursor_pos = Some(row_current_pos as usize); + self.cursor_pos = Some(cur_pos); } - if can_selection_change { + if options.can_selection_change { self.handle_selection( ui, res, cursor_state, - row_current_pos as usize, + global_pos, HexViewSelectionSide::Ascii, ); } } i += 1; + cur_pos += 1; + global_pos += 1; + } + } + + fn get_display_bytes( + &self, + data: &[u8], + file_offset: usize, + global_offset: usize, + ) -> Vec> { + let num_bytes = self.bytes_per_row * self.num_rows; + let pos: isize = global_offset as isize - file_offset as isize; + + if pos > 0 && (pos as usize) > data.len() { + vec![None; num_bytes] + } else { + let mut bytes = Vec::with_capacity(num_bytes); + for i in 0..num_bytes { + let idx = pos + i as isize; + if idx >= 0 && (idx as usize) < data.len() { + bytes.push(Some(data[idx as usize])); + } else { + bytes.push(None); + } + } + bytes } } pub fn show( &mut self, ui: &mut egui::Ui, - data: &[Option], - diffs: &Option<&[bool]>, - global_pos: usize, - file_pos: usize, + state: &HexViewState, cursor_state: CursorState, - can_selection_change: bool, - byte_grouping: ByteGrouping, + options: HexViewOptions, ) { + let data = self.get_display_bytes(state.file_data, state.file_pos, state.global_pos); + let grid_rect = egui::Grid::new(format!("hex_grid{}", self.id)) .striped(true) .spacing([0.0, 0.0]) .min_col_width(0.0) .num_columns(40) .show(ui, |ui| { - let mut current_pos = global_pos as isize - file_pos as isize; + let mut current_pos = state.global_pos as isize - state.file_pos as isize; let mut row_chunks = data.chunks(self.bytes_per_row); let mut r = 0; while r < self.num_rows { - let row = row_chunks.next().unwrap_or_default(); + let row_data = row_chunks.next().unwrap_or_default(); - let num_digits = match data.len() { - //0..=0xFFFF => 4, - 0x10000..=0xFFFFFFFF => 8, - 0x100000000..=0xFFFFFFFFFFFF => 12, - _ => 8, - }; - self.show_offset(num_digits, current_pos, ui); + self.show_offset(options.num_offset_digits, current_pos, ui); ui.add(Spacer::default().spacing_x(8.0)); ui.add(Separator::default().vertical().spacing(0.0)); ui.add(Spacer::default().spacing_x(8.0)); - self.show_hex( - ui, - byte_grouping, - current_pos, - global_pos + (r * self.bytes_per_row), - row, - diffs, - can_selection_change, - cursor_state, - ); + self.show_hex(ui, r, row_data, state, cursor_state, &options); ui.add(Spacer::default().spacing_x(8.0)); ui.add(Separator::default().vertical().spacing(0.0)); ui.add(Spacer::default().spacing_x(8.0)); - self.show_ascii(row, current_pos, ui, can_selection_change, cursor_state); + self.show_ascii(ui, r, row_data, state, cursor_state, &options); current_pos += self.bytes_per_row as isize; r += 1; @@ -370,28 +317,28 @@ impl HexView { ui: &mut egui::Ui, res: egui::Response, cursor_state: CursorState, - row_current_pos: usize, + pos: usize, side: HexViewSelectionSide, ) { if res.hovered() { - if cursor_state == CursorState::Pressed && row_current_pos > 0 { - self.selection.begin(row_current_pos, side); + if cursor_state == CursorState::Pressed { + self.selection.begin(pos, side); } - self.cursor_pos = Some(row_current_pos); + self.cursor_pos = Some(pos); } if let Some(cursor_pos) = ui.input(|i| i.pointer.hover_pos()) { - if res.rect.contains(cursor_pos) && row_current_pos > 0 { + if res.rect.contains(cursor_pos) { match cursor_state { CursorState::StillDown => { if self.selection.state == HexViewSelectionState::Selecting { - self.selection.update(row_current_pos); + self.selection.update(pos); } } CursorState::Released => { if self.selection.state == HexViewSelectionState::Selecting { - self.selection.finalize(row_current_pos); + self.selection.finalize(pos); } } _ => {} diff --git a/hex_view/src/lib.rs b/hex_view/src/lib.rs index f5f450a..f564fce 100644 --- a/hex_view/src/lib.rs +++ b/hex_view/src/lib.rs @@ -1,7 +1,9 @@ -mod hex_view; +pub mod byte_grouping; +pub mod cursor_state; +pub mod hex_view; +pub mod selection; mod spacer; pub mod theme; -pub mod byte_grouping; pub use hex_view::*; pub use theme::*; diff --git a/hex_view/src/selection.rs b/hex_view/src/selection.rs new file mode 100644 index 0000000..89b4328 --- /dev/null +++ b/hex_view/src/selection.rs @@ -0,0 +1,69 @@ +#[derive(Clone, Debug, Default, PartialEq)] +pub struct HexViewSelectionRange { + pub first: usize, + pub second: usize, +} + +#[derive(Clone, Default, Debug, PartialEq)] +pub enum HexViewSelectionState { + #[default] + None, + Selecting, + Selected, +} + +#[derive(Clone, Default, Debug, PartialEq)] +pub enum HexViewSelectionSide { + #[default] + Hex, + Ascii, +} + +#[derive(Clone, Debug, Default, PartialEq)] +pub struct HexViewSelection { + pub range: HexViewSelectionRange, + pub state: HexViewSelectionState, + pub side: HexViewSelectionSide, +} + +impl HexViewSelection { + pub fn start(&self) -> usize { + self.range.first.min(self.range.second) + } + + pub fn end(&self) -> usize { + self.range.second.max(self.range.first) + } + + pub(crate) fn contains(&self, pos: usize) -> bool { + self.state != HexViewSelectionState::None && pos >= self.start() && pos <= self.end() + } + + pub fn begin(&mut self, pos: usize, side: HexViewSelectionSide) { + self.range.first = pos; + self.range.second = pos; + self.state = HexViewSelectionState::Selecting; + self.side = side; + } + + pub fn update(&mut self, pos: usize) { + self.range.second = pos; + } + + pub fn finalize(&mut self, pos: usize) { + self.range.second = pos; + self.state = HexViewSelectionState::Selected; + } + + pub fn clear(&mut self) { + self.range.first = 0; + self.range.second = 0; + self.state = HexViewSelectionState::None; + self.side = HexViewSelectionSide::default(); + } + + pub fn adjust_cur_pos(&mut self, delta: isize) { + self.range.first = (self.range.first as isize + delta).max(0) as usize; + self.range.second = (self.range.second as isize + delta).max(0) as usize; + } +}