diff --git a/Cargo.lock b/Cargo.lock index dda151c..85c292b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -450,9 +450,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.18" +version = "4.5.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0956a43b323ac1afaffc053ed5c4b7c1f1800bacd1683c353aabbb752515dd3" +checksum = "7be5744db7978a28d9df86a214130d106a89ce49644cbc4e3f0c22c3fba30615" dependencies = [ "clap_builder", "clap_derive", @@ -460,9 +460,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.18" +version = "4.5.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d72166dd41634086d5803a47eb71ae740e61d84709c36f3c34110173db3961b" +checksum = "a5fbc17d3ef8278f55b282b2a2e75ae6f6c7d4bb70ed3d0382375104bfafdb4b" dependencies = [ "anstream", "anstyle", @@ -523,7 +523,7 @@ dependencies = [ "crossterm 0.27.0", "strum", "strum_macros", - "unicode-width 0.1.14", + "unicode-width 0.1.13", ] [[package]] @@ -558,7 +558,7 @@ dependencies = [ "encode_unicode", "lazy_static", "libc", - "unicode-width 0.1.14", + "unicode-width 0.1.13", "windows-sys 0.52.0", ] @@ -1548,7 +1548,7 @@ dependencies = [ "strum_macros", "unicode-segmentation", "unicode-truncate", - "unicode-width 0.1.14", + "unicode-width 0.1.13", ] [[package]] @@ -1847,12 +1847,12 @@ dependencies = [ [[package]] name = "terminal_size" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" +checksum = "4f599bd7ca042cfdf8f4512b277c02ba102247820f9d9d4a9f521f496751a6ef" dependencies = [ "rustix", - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] @@ -1863,7 +1863,7 @@ checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" dependencies = [ "smawk", "unicode-linebreak", - "unicode-width 0.1.14", + "unicode-width 0.1.13", ] [[package]] @@ -1936,7 +1936,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd137780d743c103a391e06fe952487f914b299a4fe2c3626677f6a6339a7c6b" dependencies = [ "ratatui", - "unicode-width 0.1.14", + "unicode-width 0.1.13", ] [[package]] @@ -1992,14 +1992,14 @@ checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" dependencies = [ "itertools", "unicode-segmentation", - "unicode-width 0.1.14", + "unicode-width 0.1.13", ] [[package]] name = "unicode-width" -version = "0.1.14" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" [[package]] name = "unicode-width" @@ -2271,15 +2271,6 @@ dependencies = [ "windows-targets 0.42.2", ] -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - [[package]] name = "windows-sys" version = "0.52.0" diff --git a/examples/demo.rs b/examples/demo.rs index cc831b5..085521b 100644 --- a/examples/demo.rs +++ b/examples/demo.rs @@ -1,6 +1,6 @@ //! A simple demo of how to use the binsider as a library. -use std::{env, fs, path::PathBuf, sync::mpsc, time::Duration}; +use std::{fs, path::PathBuf, sync::mpsc, time::Duration}; use binsider::{prelude::*, tui::ui::Tab}; @@ -11,7 +11,11 @@ use ratatui::{ fn main() -> Result<()> { // Create an analyzer. - let path = PathBuf::from(env::args().next().expect("no file given")); + let mut path = PathBuf::from("ls"); + if !path.exists() { + let resolved_path = which::which(path.to_string_lossy().to_string()).unwrap(); + path = resolved_path; + } let file_data = fs::read(&path)?; let file_info = FileInfo::new( path.to_str().unwrap_or_default(), diff --git a/examples/widget.rs b/examples/widget.rs new file mode 100644 index 0000000..fd42511 --- /dev/null +++ b/examples/widget.rs @@ -0,0 +1,64 @@ +use std::path::PathBuf; +use ratatui::{ + crossterm::event::{KeyCode}, + Frame, +}; +use binsider::prelude::*; + +fn main() -> Result<()> { + let mut path = PathBuf::from("ls"); + if !path.exists() { + let resolved_path = which::which(path.to_string_lossy().to_string())?; + path = resolved_path; + } + + let file_data = std::fs::read(&path)?; + let bytes = file_data.as_slice(); + let file_info = FileInfo::new(path.to_str().expect("should be valid string"), Some(vec![]), bytes)?; + let analyzer = + Analyzer::new(file_info, 15, vec![])?; + + // Create an application. + let mut state = State::new(analyzer)?; + let events = EventHandler::new(250); + state.analyzer.extract_strings(events.sender.clone()); + + let mut terminal = ratatui::init(); + loop { + terminal + .draw(|frame: &mut Frame| { + render(&mut state, frame); + }) + .expect("failed to draw frame"); + + let event = events.next()?; + match event { + Event::Key(key_event) => { + if key_event.code == KeyCode::Char('q') { + break; + } + handle_event(Event::Key(key_event), &events, &mut state)?; + } + Event::Restart(None) => { + break; + } + Event::Restart(Some(path)) => { + let file_data = std::fs::read(&path)?; + let bytes = file_data.as_slice(); + let file_info = FileInfo::new(path.to_str().expect("should be valid string"), Some(vec![]), bytes)?; + let analyzer = + Analyzer::new(file_info, 15, vec![])?; + state.analyzer = analyzer; + state.handle_tab()?; + state.analyzer.extract_strings(events.sender.clone()); + } + _ => { + handle_event(event, &events, &mut state)?; + } + } + } + events.stop(); + ratatui::restore(); + + Ok(()) +} \ No newline at end of file diff --git a/src/app.rs b/src/app.rs index 61e7688..46cbb6e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -31,7 +31,7 @@ pub struct Analyzer<'a> { /// List of files that are being analyzed. pub files: Vec, /// Current file information. - pub file: FileInfo<'a>, + pub file: FileInfo, /// Elf properties. pub elf: Elf, /// Strings. @@ -59,11 +59,11 @@ impl Debug for Analyzer<'_> { impl<'a> Analyzer<'a> { /// Constructs a new instance. pub fn new( - mut file_info: FileInfo<'a>, + mut file_info: FileInfo, strings_len: usize, files: Vec, ) -> Result { - let elf_bytes = ElfBytes::::minimal_parse(file_info.bytes)?; + let elf_bytes = ElfBytes::::minimal_parse(file_info.bytes.as_ref())?; let elf = Elf::try_from(elf_bytes)?; let heh = Heh::new(file_info.open_file()?, Encoding::Ascii, 0) .map_err(|e| Error::HexdumpError(e.to_string()))?; @@ -81,9 +81,9 @@ impl<'a> Analyzer<'a> { } /// Extracts the library dependencies. - pub fn extract_libs(file_info: &FileInfo<'a>) -> Result> { + pub fn extract_libs(file_info: &FileInfo) -> Result> { let mut dependencies = DependencyAnalyzer::default() - .analyze(file_info.path)? + .analyze(&file_info.path)? .libraries .clone() .into_iter() diff --git a/src/file.rs b/src/file.rs index bd1be43..0716fe5 100644 --- a/src/file.rs +++ b/src/file.rs @@ -16,13 +16,13 @@ use std::os::windows::fs::MetadataExt; /// General file information. #[derive(Debug)] -pub struct FileInfo<'a> { +pub struct FileInfo { /// Path of the file. - pub path: &'a str, + pub path: String, /// Arguments of the file. - pub arguments: Option>, + pub arguments: Option>, /// Bytes of the file. - pub bytes: &'a [u8], + pub bytes: Box<[u8]>, /// Whether if the file is read only. pub is_read_only: bool, /// Name of the file. @@ -69,19 +69,19 @@ pub struct FileDateInfo { pub birth: String, } -impl<'a> FileInfo<'a> { +impl FileInfo { /// Constructs a new instance. #[cfg(not(target_os = "windows"))] - pub fn new(path: &'a str, arguments: Option>, bytes: &'a [u8]) -> Result { + pub fn new(path: &str, arguments: Option>, bytes: impl Into>) -> Result { let metadata = fs::metadata(path)?; let mode = metadata.permissions().mode(); let users = Users::new_with_refreshed_list(); let groups = Groups::new_with_refreshed_list(); Ok(Self { - path, + path: path.to_string(), arguments, - bytes, + bytes: bytes.into(), is_read_only: false, name: PathBuf::from(path) .file_name() @@ -160,11 +160,11 @@ impl<'a> FileInfo<'a> { /// Opens the file (with R/W if possible) and returns it. pub fn open_file(&mut self) -> Result { Ok( - match OpenOptions::new().write(true).read(true).open(self.path) { + match OpenOptions::new().write(true).read(true).open(&self.path) { Ok(v) => v, Err(_) => { self.is_read_only = true; - File::open(self.path)? + File::open(&self.path)? } }, ) diff --git a/src/lib.rs b/src/lib.rs index cd36569..5d096f0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,7 +35,9 @@ use prelude::*; use ratatui::backend::CrosstermBackend; use ratatui::Terminal; use std::{env, fs, io, path::PathBuf}; -use tui::{state::State, ui::Tab, Tui}; +use std::sync::atomic::Ordering; +use tui::{state::State, ui::Tab}; +use crate::tui::{tui_exit, tui_init, ui}; /// Runs binsider. pub fn run(mut args: Args) -> Result<()> { @@ -48,7 +50,7 @@ pub fn run(mut args: Args) -> Result<()> { let mut parts = path_str.split_whitespace(); if let Some(bin) = parts.next() { path = PathBuf::from(bin); - arguments = Some(parts.collect()); + arguments = Some(parts.map(|s| s.to_string()).collect()); } if !path.exists() { let resolved_path = which::which(path.to_string_lossy().to_string())?; @@ -58,81 +60,101 @@ pub fn run(mut args: Args) -> Result<()> { path = resolved_path; } let file_data = fs::read(&path)?; - let bytes = file_data.as_slice(); - let file_info = FileInfo::new(path.to_str().unwrap_or_default(), arguments, bytes)?; + let file_info = FileInfo::new(path.to_str().unwrap_or_default(), arguments, file_data)?; let analyzer = Analyzer::new(file_info, args.min_strings_len, args.files.clone())?; start_tui(analyzer, args) } +/// Generic handler for `binsider events. +/// +/// Can be used for user defined widgets. Any event handler can be redeclared just by ignore this +/// function. +pub fn handle_event(event: Event, events: &EventHandler, state: &mut State) -> Result<()> { + match event { + Event::Tick => {} + Event::Key(key_event) => { + let command = if state.input_mode { + Command::Input(InputCommand::parse(key_event, &state.input)) + } else if state.show_heh { + Command::Hexdump(HexdumpCommand::parse( + key_event, + state.analyzer.file.is_read_only, + )) + } else { + Command::from(key_event) + }; + state.run_command(command, events.sender.clone())?; + } + Event::Mouse(mouse_event) => { + state.run_command(Command::from(mouse_event), events.sender.clone())?; + } + Event::Resize(_, _) => {} + Event::FileStrings(strings) => { + state.strings_loaded = true; + state.analyzer.strings = Some(strings?.into_iter().map(|(v, l)| (l, v)).collect()); + if state.tab == Tab::Strings { + state.handle_tab()?; + } + } + #[cfg(feature = "dynamic-analysis")] + Event::Trace => { + state.system_calls_loaded = false; + tracer::trace_syscalls(&state.analyzer.file, events.sender.clone()); + } + #[cfg(feature = "dynamic-analysis")] + Event::TraceResult(syscalls) => { + state.analyzer.tracer = syscalls.unwrap_or_else(|e| TraceData { + syscalls: console::style(e).red().to_string().as_bytes().to_vec(), + ..Default::default() + }); + state.system_calls_loaded = true; + state.dynamic_scroll_index = 0; + state.handle_tab()?; + } + #[cfg(not(feature = "dynamic-analysis"))] + Event::Trace | Event::TraceResult(_) => {} + Event::Restart(_) => {} + } + Ok(()) +} + + /// Starts the terminal user interface. pub fn start_tui(analyzer: Analyzer, args: Args) -> Result<()> { // Create an application. let mut state = State::new(analyzer)?; - // Change tab depending on cli arguments. state.set_tab(args.tab); // Initialize the terminal user interface. let backend = CrosstermBackend::new(io::stderr()); - let terminal = Terminal::new(backend)?; + let mut terminal = Terminal::new(backend)?; let events = EventHandler::new(250); state.analyzer.extract_strings(events.sender.clone()); - let mut tui = Tui::new(terminal, events); - tui.init()?; + tui_init(&mut terminal)?; // Start the main loop. while state.running { // Render the user interface. - tui.draw(&mut state)?; + terminal.draw(|frame| ui::render(&mut state, frame))?; // Handle events. - match tui.events.next()? { - Event::Tick => {} - Event::Key(key_event) => { - let command = if state.input_mode { - Command::Input(InputCommand::parse(key_event, &state.input)) - } else if state.show_heh { - Command::Hexdump(HexdumpCommand::parse( - key_event, - state.analyzer.file.is_read_only, - )) - } else { - Command::from(key_event) - }; - state.run_command(command, tui.events.sender.clone())?; - } - Event::Mouse(mouse_event) => { - state.run_command(Command::from(mouse_event), tui.events.sender.clone())?; - } - Event::Resize(_, _) => {} - Event::FileStrings(strings) => { - state.strings_loaded = true; - state.analyzer.strings = Some(strings?.into_iter().map(|(v, l)| (l, v)).collect()); - if state.tab == Tab::Strings { - state.handle_tab()?; - } - } + + let event = events.next()?; + match event { #[cfg(feature = "dynamic-analysis")] Event::Trace => { - state.system_calls_loaded = false; - tui.toggle_pause()?; - tracer::trace_syscalls(&state.analyzer.file, tui.events.sender.clone()); + events + .key_input_disabled + .store(true, Ordering::Relaxed); + handle_event(event, &events, &mut state)?; } #[cfg(feature = "dynamic-analysis")] - Event::TraceResult(syscalls) => { - state.analyzer.tracer = match syscalls { - Ok(v) => v, - Err(e) => TraceData { - syscalls: console::style(e).red().to_string().as_bytes().to_vec(), - ..Default::default() - }, - }; - state.system_calls_loaded = true; - state.dynamic_scroll_index = 0; - tui.toggle_pause()?; - state.handle_tab()?; + Event::TraceResult(_) => { + events + .key_input_disabled + .store(false, Ordering::Relaxed); + handle_event(event, &events, &mut state)?; } - #[cfg(not(feature = "dynamic-analysis"))] - Event::Trace | Event::TraceResult(_) => {} Event::Restart(path) => { let mut args = args.clone(); match path { @@ -144,15 +166,34 @@ pub fn start_tui(analyzer: Analyzer, args: Args) -> Result<()> { } } if !args.files.is_empty() { - tui.exit()?; - state.running = false; - run(args)?; + if args.files.is_empty() { + args.files.push(env::current_exe()?); + } + let mut path = args.files[args.files.len() - 1].clone(); + if !path.exists() { + let resolved_path = which::which(path.to_string_lossy().to_string())?; + if let Some(file) = args.files.iter_mut().find(|f| **f == path) { + *file = resolved_path.clone(); + } + path = resolved_path; + } + let file_data = fs::read(&path)?; + let file_info = FileInfo::new(path.to_str().unwrap_or_default(), Some(vec![]), file_data)?; + let analyzer = Analyzer::new(file_info, args.min_strings_len, args.files.clone())?; + + state.analyzer = analyzer; + state.analyzer.extract_strings(events.sender.clone()); + state.handle_tab()?; } } + _ => { + handle_event(event, &events, &mut state)?; + } } } // Exit the user interface. - tui.exit()?; + tui_exit(&mut terminal)?; + events.stop(); Ok(()) } diff --git a/src/prelude.rs b/src/prelude.rs index fcc6415..e7e3d55 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -5,3 +5,4 @@ pub use super::tui::command::*; pub use super::tui::event::*; pub use super::tui::state::*; pub use super::tui::ui::*; +pub use super::handle_event; diff --git a/src/tui/mod.rs b/src/tui/mod.rs index 9688b78..f419558 100644 --- a/src/tui/mod.rs +++ b/src/tui/mod.rs @@ -14,104 +14,36 @@ pub mod widgets; pub mod command; use crate::error::Result; -use event::EventHandler; -use ratatui::backend::{Backend, CrosstermBackend}; +use ratatui::backend::Backend; use ratatui::crossterm::event::{DisableMouseCapture, EnableMouseCapture}; use ratatui::crossterm::terminal::{self, EnterAlternateScreen, LeaveAlternateScreen}; use ratatui::Terminal; -use state::State; -use std::sync::atomic::Ordering; use std::{io, panic}; -/// Representation of a terminal user interface. +/// Initializes the terminal interface. /// -/// It is responsible for setting up the terminal, -/// initializing the interface and handling the draw events. -#[derive(Debug)] -pub struct Tui { - /// Interface to the Terminal. - terminal: Terminal, - /// Terminal event handler. - pub events: EventHandler, - /// Is the interface paused? - pub paused: bool, +/// It enables the raw mode and sets terminal properties. +pub fn tui_init(terminal: &mut Terminal) -> Result<()> { + terminal::enable_raw_mode()?; + ratatui::crossterm::execute!(io::stderr(), EnterAlternateScreen, EnableMouseCapture)?; + panic::set_hook(Box::new(move |panic| { + better_panic::Settings::auto() + .most_recent_first(false) + .lineno_suffix(true) + .create_panic_handler()(panic); + std::process::exit(1); + })); + terminal.hide_cursor()?; + terminal.clear()?; + Ok(()) } -impl Tui { - /// Constructs a new instance of [`Tui`]. - pub fn new(terminal: Terminal, events: EventHandler) -> Self { - Self { - terminal, - events, - paused: false, - } - } - - /// Initializes the terminal interface. - /// - /// It enables the raw mode and sets terminal properties. - pub fn init(&mut self) -> Result<()> { - terminal::enable_raw_mode()?; - ratatui::crossterm::execute!(io::stderr(), EnterAlternateScreen, EnableMouseCapture)?; - panic::set_hook(Box::new(move |panic| { - Self::reset().expect("failed to reset the terminal"); - better_panic::Settings::auto() - .most_recent_first(false) - .lineno_suffix(true) - .create_panic_handler()(panic); - std::process::exit(1); - })); - self.terminal.hide_cursor()?; - self.terminal.clear()?; - Ok(()) - } - - /// [`Draw`] the terminal interface by [`rendering`] the widgets. - /// - /// [`Draw`]: tui::Terminal::draw - /// [`rendering`]: crate::ui:render - pub fn draw(&mut self, app: &mut State) -> Result<()> { - self.terminal.draw(|frame| ui::render(app, frame))?; - Ok(()) - } - - /// Toggles the [`paused`] state of interface. - /// - /// It disables the key input and exits the - /// terminal interface on pause (and vice-versa). - /// - /// [`paused`]: Tui::paused - pub fn toggle_pause(&mut self) -> Result<()> { - self.paused = !self.paused; - if self.paused { - Self::reset()?; - } else { - self.init()?; - } - self.events - .key_input_disabled - .store(self.paused, Ordering::Relaxed); - Ok(()) - } - - /// Reset the terminal interface. - /// - /// It disables the raw mode and reverts back the terminal properties. - pub fn reset() -> Result<()> { - terminal::disable_raw_mode()?; - ratatui::crossterm::execute!(io::stderr(), LeaveAlternateScreen, DisableMouseCapture)?; - Terminal::new(CrosstermBackend::new(io::stderr()))?.show_cursor()?; - Ok(()) - } - - /// Exits the terminal interface. - /// - /// It disables the raw mode and reverts back the terminal properties. - pub fn exit(&mut self) -> Result<()> { - terminal::disable_raw_mode()?; - ratatui::crossterm::execute!(io::stderr(), LeaveAlternateScreen, DisableMouseCapture)?; - self.terminal.show_cursor()?; - self.events.stop(); - Ok(()) - } -} +/// Exits the terminal interface. +/// +/// It disables the raw mode and reverts back the terminal properties. +pub fn tui_exit(terminal: &mut Terminal) -> Result<()> { + terminal::disable_raw_mode()?; + ratatui::crossterm::execute!(io::stderr(), LeaveAlternateScreen, DisableMouseCapture)?; + terminal.show_cursor()?; + Ok(()) +} \ No newline at end of file diff --git a/src/tui/state.rs b/src/tui/state.rs index 8276bc0..dd4d3a8 100644 --- a/src/tui/state.rs +++ b/src/tui/state.rs @@ -91,7 +91,7 @@ impl<'a> State<'a> { state.handle_tab()?; Ok(state) } - + /// Runs a command and updates the state. pub fn run_command( &mut self,