hex_patch/app/files/
files.rs

1#![allow(clippy::module_inception)]
2use std::error::Error;
3
4use ratatui::{backend::Backend, Terminal};
5
6use crate::{
7    app::{
8        data::Data, info_mode::InfoMode, log::NotificationLevel, popup::popup_state::PopupState,
9        App,
10    },
11    get_app_context,
12    headers::Header,
13};
14
15use super::{filesystem::FileSystem, path, path_result::PathResult};
16
17impl App {
18    pub(in crate::app) fn go_to_path<B: Backend>(
19        &mut self,
20        currently_open_path: &str,
21        path: &str,
22        scroll: usize,
23        popup: &mut Option<PopupState>,
24        terminal: &mut Terminal<B>,
25    ) -> Result<(), Box<dyn Error>> {
26        let contents = Self::find_dir_contents(currently_open_path, path, &self.filesystem)?;
27        if contents.is_empty() {
28            return Err(format!("No files found that matches \"{}\"", path).into());
29        }
30        let selected = contents
31            .into_iter()
32            .nth(scroll)
33            .expect("Scroll out of bounds for go_to_path.");
34
35        if self.filesystem.is_dir(selected.path()) {
36            Self::open_dir(popup, selected.path(), &mut self.filesystem)?;
37        } else {
38            self.open_file(selected.path(), terminal)?;
39            *popup = None;
40        }
41
42        Ok(())
43    }
44
45    pub(in crate::app) fn get_current_dir(&self) -> String {
46        let current_path = self.filesystem.pwd();
47        if self.filesystem.is_dir(current_path) {
48            current_path.to_owned()
49        } else {
50            path::parent(current_path)
51                .expect("A file should have a parent directory.")
52                .to_owned()
53        }
54    }
55
56    pub(in crate::app) fn find_dir_contents(
57        currently_open_path: &str,
58        path: &str,
59        filesystem: &FileSystem,
60    ) -> Result<Vec<PathResult>, Box<dyn Error>> {
61        let mut ret = Vec::new();
62        let (selected_dir, file_name) = if path::is_absolute(path) {
63            if filesystem.is_dir(path) {
64                (filesystem.canonicalize(path)?, "".to_string())
65            } else if let Some(parent) = path::parent(path) {
66                if filesystem.is_dir(parent) {
67                    (
68                        filesystem.canonicalize(parent)?,
69                        path::filename(path).map_or("".into(), |name| name.to_string()),
70                    )
71                } else {
72                    (currently_open_path.to_string(), path.to_string())
73                }
74            } else {
75                (currently_open_path.to_string(), path.to_string())
76            }
77        } else {
78            (currently_open_path.to_string(), path.to_string())
79        };
80
81        let entries = filesystem.ls(&selected_dir)?;
82        let entries = entries
83            .into_iter()
84            .map(|entry| path::diff(&entry, &selected_dir).to_string())
85            .collect::<Vec<_>>();
86
87        let entries = entries
88            .into_iter()
89            .filter(|entry| entry.to_lowercase().starts_with(&file_name.to_lowercase()));
90
91        for entry in entries {
92            if let Ok(result) = PathResult::new(
93                &path::join(&selected_dir, &entry, filesystem.separator()),
94                filesystem,
95            ) {
96                ret.push(result);
97            }
98        }
99
100        Ok(ret)
101    }
102
103    pub(in crate::app) fn open_dir(
104        popup: &mut Option<PopupState>,
105        path: &str,
106        filesystem: &mut FileSystem,
107    ) -> Result<(), Box<dyn Error>> {
108        let path = filesystem.canonicalize(path)?;
109        *popup = Some(PopupState::Open {
110            currently_open_path: path.clone(),
111            path: "".into(),
112            cursor: 0,
113            results: Self::find_dir_contents(&path, "", filesystem)?,
114            scroll: 0,
115        });
116        Ok(())
117    }
118
119    pub fn log_header_info(&mut self) {
120        if self.header != Header::None {
121            match &self.header {
122                Header::GenericHeader(header) => self.log(
123                    NotificationLevel::Info,
124                    &format!("File type: {:?}", header.file_type()),
125                ),
126                // TODO: maybe add info for a more detailed log
127                Header::CustomHeader(_) => self.log(NotificationLevel::Info, "File type: Custom"),
128                Header::None => unreachable!(),
129            }
130            self.log(
131                NotificationLevel::Info,
132                &format!("Architecture: {:?}", self.header.architecture()),
133            );
134            self.log(
135                NotificationLevel::Info,
136                &format!("Bitness: {}", self.header.bitness()),
137            );
138            self.log(
139                NotificationLevel::Info,
140                &format!("Entry point: {:#X}", self.header.entry_point()),
141            );
142            for section in self.header.get_sections() {
143                self.log(NotificationLevel::Info, &format!("Section: {}", section));
144            }
145        } else {
146            self.log(NotificationLevel::Info, "No header found. Assuming 64-bit.");
147        }
148
149        self.log(
150            NotificationLevel::Info,
151            &format!(
152                "Press {} for a list of commands.",
153                Self::key_event_to_string(self.settings.key.help)
154            ),
155        );
156    }
157
158    pub(in crate::app) fn open_file<B: Backend>(
159        &mut self,
160        path: &str,
161        terminal: &mut Terminal<B>,
162    ) -> Result<(), Box<dyn Error>> {
163        self.log(
164            NotificationLevel::Info,
165            &format!("Opening file: \"{}\"", path),
166        );
167
168        self.filesystem.cd(path);
169        self.info_mode = InfoMode::Text;
170        self.scroll = 0;
171        self.cursor = (0, 0);
172
173        self.screen_size = Self::get_size(terminal)?;
174        self.block_size = 8;
175        self.vertical_margin = 2;
176        self.blocks_per_row = Self::calc_blocks_per_row(
177            self.block_size,
178            self.screen_size.0,
179            self.fullscreen,
180            self.selected_pane,
181        );
182
183        Self::print_loading_status(
184            &self.settings.color,
185            &format!("Opening \"{}\"...", path),
186            terminal,
187        )?;
188        self.data = Data::new(
189            self.filesystem.read(self.filesystem.pwd())?,
190            self.settings.app.history_limit,
191        );
192
193        self.load_comments(None);
194
195        Self::print_loading_status(&self.settings.color, "Decoding binary data...", terminal)?;
196
197        self.header = self.parse_header();
198
199        Self::print_loading_status(
200            &self.settings.color,
201            "Disassembling executable...",
202            terminal,
203        )?;
204
205        (self.assembly_offsets, self.assembly_instructions) =
206            Self::sections_from_bytes(self.data.bytes(), &self.header);
207
208        Self::print_loading_status(&self.settings.color, "Opening ui...", terminal)?;
209        self.log_header_info();
210        let mut app_context = get_app_context!(self);
211        self.plugin_manager.on_open(&mut app_context);
212
213        Ok(())
214    }
215
216    pub(in crate::app) fn save_file_as(&mut self, path: &str) -> Result<(), Box<dyn Error>> {
217        if let Some(parent) = path::parent(path) {
218            self.filesystem.mkdirs(parent)?;
219        };
220
221        self.filesystem.create(path)?;
222        self.filesystem.cd(&self.filesystem.canonicalize(path)?);
223        self.save_file()?;
224        Ok(())
225    }
226
227    pub(in crate::app) fn save_file(&mut self) -> Result<(), Box<dyn Error>> {
228        let mut app_context = get_app_context!(self);
229        self.plugin_manager.on_save(&mut app_context);
230        self.filesystem
231            .write(self.filesystem.pwd(), self.data.bytes())?;
232        self.data.reset_dirty();
233        match &self.filesystem {
234            FileSystem::Local { path } => {
235                self.log(NotificationLevel::Info, &format!("Saved to {}", path));
236            }
237            FileSystem::Remote { path, connection } => {
238                self.log(
239                    NotificationLevel::Info,
240                    &format!("Saved to {} at {}", path, connection),
241                );
242            }
243        }
244
245        Ok(())
246    }
247}