use std::{
collections::{HashMap, HashSet},
fs::{File, OpenOptions},
io::{self, Read, Seek, SeekFrom, Write},
path::Path,
};
use arboard::Clipboard;
use ratatui::{Frame, layout::Rect, widgets::ListState};
use tui_input::Input;
use crate::{
config::*,
editor::*,
global::calculator::Calculator,
hex::{hex_view::HexView, strings::FoundString},
reader::Reader,
themes::*,
};
#[derive(Default)]
pub struct FileInfo {
pub file: Option<File>,
pub path: String,
pub is_read_only: bool,
pub name: String,
pub r#type: &'static str,
pub size: usize,
}
#[derive(Debug)]
pub struct TextView {
pub area_height: u16,
pub lines_to_show: usize,
pub scroll_offset: (u16, u16), pub table: &'static encoding_rs::Encoding,
}
pub struct App {
pub buffer: [u8; APP_CACHE_SIZE],
pub calculator: Calculator,
pub clipboard: Result<Clipboard, arboard::Error>,
pub command_area: Rect,
pub command_input: Input,
pub config: Config,
pub dialog_2nd_renderer: Option<fn(&mut App, &mut Frame)>,
pub dialog_renderer: Option<fn(&mut App, &mut Frame)>,
pub editor_view: AppView,
pub file_info: FileInfo,
pub hex_view: HexView,
pub list_state: ListState,
pub log_scroll_offset: (u16, u16),
pub logs: Vec<String>,
pub reader: Reader,
pub running: bool,
pub state: UIState,
pub string_regex: String,
pub strings: Vec<FoundString>,
pub text_view: TextView,
}
impl App {
pub fn new() -> Self {
App {
buffer: [0u8; APP_CACHE_SIZE],
calculator: Calculator::default(),
clipboard: Clipboard::new(),
command_area: Rect::default(),
command_input: Input::default(),
config: Config {
database: true,
dim_control_chars: false,
dim_zeroes: true,
hex_mode_bytes_per_line: 16,
hex_mode_non_graphic_char: '.',
maximum_strings_to_show: 3000,
minimum_string_length: 4,
theme: DARK,
},
dialog_renderer: None,
dialog_2nd_renderer: None,
editor_view: AppView::Hex,
file_info: FileInfo::default(),
hex_view: HexView {
editing_hex: true,
highlights: HashSet::with_capacity(8),
..Default::default()
},
list_state: ListState::default(),
log_scroll_offset: (0, 0),
logs: Vec::with_capacity(100),
reader: Reader::new(),
running: true,
state: UIState::Normal,
string_regex: String::new(),
strings: Vec::new(),
text_view: TextView {
area_height: 0,
lines_to_show: 0,
scroll_offset: (0, 0),
table: encoding_rs::UTF_8,
},
}
}
fn id_file(&mut self) {
self.file_info.r#type = match self.buffer[0] {
0x7f => "ELF",
0xca | 0xcf => "Mach-O",
0x4d => "PE",
_ => "",
}
}
pub fn load_file(
&mut self,
filepath: &str,
initial_offset: usize,
read_only: bool,
) -> io::Result<()> {
let path = Path::new(&filepath);
if let Some(f) = path.file_name()
&& let Some(fname) = f.to_str()
{
self.file_info.name = String::from(fname);
self.file_info.path = String::from(filepath);
}
let meta = path.metadata()?;
if !read_only && let Ok(file) = OpenOptions::new().read(true).write(true).open(path) {
self.file_info.file = Some(file);
} else {
let file = OpenOptions::new().read(true).open(path)?;
self.file_info.file = Some(file);
self.file_info.is_read_only = true;
}
if let Some(f) = self.file_info.file.as_mut() {
self.file_info.size = meta.len() as usize;
self.reader.cache_blocks = self.file_info.size.div_ceil(APP_CACHE_SIZE);
self.reader.pages = self.file_info.size.div_ceil(APP_PAGE_SIZE);
self.reader.page_last = self.reader.pages.saturating_sub(1);
let _bytes_read = f.read(&mut self.buffer)?;
self.id_file();
self.log(format!(
"filesize: {} (0x{:x})",
self.file_info.size, self.file_info.size
));
}
if initial_offset != 0 {
self.goto(0);
}
self.goto(initial_offset);
if self.config.database {
let _ = self.load_database();
}
Ok(())
}
pub fn reload_file(&mut self) {
let fp = self.file_info.path.clone();
self.load_file(&fp, self.hex_view.offset, self.file_info.is_read_only)
.expect("could not reload the file");
}
pub fn read_chunk_from_file(&mut self, nblock: usize) -> io::Result<()> {
if self.file_info.file.is_none() {
return Err(std::io::Error::other("file not open"));
}
if let Some(f) = &mut self.file_info.file {
f.seek(SeekFrom::Start((nblock * APP_CACHE_SIZE) as u64))?;
let _ = f.read(&mut self.buffer)?;
self.reader.cache_block_number = nblock;
self.log(format!("read_chunk_from_file({nblock})"));
}
Ok(())
}
pub fn write_to_buffer(&mut self, changed: HashMap<usize, String>) {
let ofs = self.hex_view.offset;
let mut total_written = 0usize;
for (k, v) in &changed {
self.read_chunk_for_offset(*k);
if let Ok(b) = u8::from_str_radix(v, 16) {
let buf_ofs = k % APP_CACHE_SIZE;
self.buffer[buf_ofs] = b;
total_written += 1;
}
}
App::log(
self,
format!("{} bytes written to buffer: {:?}", total_written, changed),
);
self.goto(ofs);
}
pub fn write_to_file(&mut self) -> io::Result<()> {
if self.file_info.file.is_none() {
return Err(io::Error::other("file not open"));
}
let mut total_written = 0;
if let Some(f) = &mut self.file_info.file {
for (k, v) in &self.hex_view.changed_bytes {
f.seek(SeekFrom::Start(*k as u64))?;
if let Ok(b) = u8::from_str_radix(v, 16) {
let buf = vec![b];
let written = f.write(&buf)?;
if written != 1 {
return Err(io::Error::other("could not write to file"));
}
total_written += written;
}
}
}
App::log(self, format!("{} bytes written to file", total_written));
self.hex_view.changed_bytes.clear();
Ok(())
}
pub fn read_u8(&mut self, offset: usize) -> Option<u8> {
let ofs = offset % APP_CACHE_SIZE;
if offset >= self.file_info.size {
return None;
}
Some(self.buffer[ofs])
}
pub fn read_i8(&mut self, offset: usize) -> Option<i8> {
let ofs = offset % APP_CACHE_SIZE;
if offset >= self.file_info.size {
return None;
}
Some(self.buffer[ofs] as i8)
}
pub fn read_u16(&mut self, offset: usize) -> Option<u16> {
let ofs = offset % APP_CACHE_SIZE;
if offset + 1 >= self.file_info.size {
return None;
}
let b1 = self.buffer[ofs];
let b2 = self.buffer[ofs + 1];
Some(u16::from_le_bytes([b1, b2]))
}
pub fn read_i16(&mut self, offset: usize) -> Option<i16> {
let ofs = offset % APP_CACHE_SIZE;
if offset + 1 >= self.file_info.size {
return None;
}
let b1 = self.buffer[ofs];
let b2 = self.buffer[ofs + 1];
Some(i16::from_le_bytes([b1, b2]))
}
pub fn read_u32(&mut self, offset: usize) -> Option<u32> {
let ofs = offset % APP_CACHE_SIZE;
if offset + 3 >= self.file_info.size {
return None;
}
let b1 = self.buffer[ofs];
let b2 = self.buffer[ofs + 1];
let b3 = self.buffer[ofs + 2];
let b4 = self.buffer[ofs + 3];
Some(u32::from_le_bytes([b1, b2, b3, b4]))
}
pub fn read_i32(&mut self, offset: usize) -> Option<i32> {
let ofs = offset % APP_CACHE_SIZE;
if offset + 3 >= self.file_info.size {
return None;
}
let b1 = self.buffer[ofs];
let b2 = self.buffer[ofs + 1];
let b3 = self.buffer[ofs + 2];
let b4 = self.buffer[ofs + 3];
Some(i32::from_le_bytes([b1, b2, b3, b4]))
}
pub fn read_u64(&mut self, offset: usize) -> Option<u64> {
let ofs = offset % APP_CACHE_SIZE;
if offset + 7 >= self.file_info.size {
return None;
}
let b1 = self.buffer[ofs];
let b2 = self.buffer[ofs + 1];
let b3 = self.buffer[ofs + 2];
let b4 = self.buffer[ofs + 3];
let b5 = self.buffer[ofs + 4];
let b6 = self.buffer[ofs + 5];
let b7 = self.buffer[ofs + 6];
let b8 = self.buffer[ofs + 7];
Some(u64::from_le_bytes([b1, b2, b3, b4, b5, b6, b7, b8]))
}
pub fn read_i64(&mut self, offset: usize) -> Option<i64> {
let ofs = offset % APP_CACHE_SIZE;
if offset + 7 >= self.file_info.size {
return None;
}
let b1 = self.buffer[ofs];
let b2 = self.buffer[ofs + 1];
let b3 = self.buffer[ofs + 2];
let b4 = self.buffer[ofs + 3];
let b5 = self.buffer[ofs + 4];
let b6 = self.buffer[ofs + 5];
let b7 = self.buffer[ofs + 6];
let b8 = self.buffer[ofs + 7];
Some(i64::from_le_bytes([b1, b2, b3, b4, b5, b6, b7, b8]))
}
pub fn read_chunk_for_offset(&mut self, offset: usize) {
let nblock = offset / APP_CACHE_SIZE;
if offset > self.reader.cache_end {
self.read_chunk_from_file(nblock).unwrap();
self.reader.cache_start += nblock * APP_CACHE_SIZE;
self.reader.cache_end += nblock * APP_CACHE_SIZE;
} else if offset < self.reader.cache_start {
self.read_chunk_from_file(nblock).unwrap();
self.reader.cache_start -= APP_CACHE_SIZE;
self.reader.cache_end -= APP_CACHE_SIZE;
} else if offset == 0 {
self.reader.cache_start = 0;
self.reader.cache_end = APP_CACHE_SIZE - 1;
self.reader.page_start = 0;
self.reader.page_end = APP_PAGE_SIZE - 1;
}
}
}