diff --git a/Cargo.toml b/Cargo.toml index 3468d28..8d2ad76 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ object = "0.35.0" pathdiff = "0.2.1" pdb = "0.8.0" ratatui = { version = "0.26.3", features = ["serde"] } +regex = "1.10.4" russh = "0.43.0" russh-keys = "0.43.0" russh-sftp = "2.0.0" diff --git a/src/app/files/files.rs b/src/app/files/files.rs index 99db858..ec38b0f 100644 --- a/src/app/files/files.rs +++ b/src/app/files/files.rs @@ -5,7 +5,7 @@ use ratatui::{backend::Backend, Terminal}; use crate::{app::{info_mode::InfoMode, notification::NotificationLevel, popup_state::PopupState, App}, headers::Header}; -use super::{filesystem::FileSystem, path_result::PathResult, str_path::{path_diff, path_filename, path_is_absolute, path_join, path_parent}}; +use super::{filesystem::FileSystem, path_result::PathResult, path}; impl App { @@ -47,24 +47,24 @@ impl App } else { - path_parent(current_path).expect("A file should have a parent directory.").to_owned() + path::parent(current_path).expect("A file should have a parent directory.").to_owned() } } pub(in crate::app) fn find_dir_contents(currently_open_path: &str, path: &str, filesystem: &FileSystem) -> Result, Box> { let mut ret = Vec::new(); - let (selected_dir, file_name) = if path_is_absolute(path) + let (selected_dir, file_name) = if path::is_absolute(path) { if filesystem.is_dir(path) { (filesystem.canonicalize(path)?, "".to_string()) } - else if let Some(parent) = path_parent(path) + else if let Some(parent) = path::parent(path) { if filesystem.is_dir(parent) { - (filesystem.canonicalize(parent)?, path_filename(path).map_or("".into(), |name| name.to_string())) + (filesystem.canonicalize(parent)?, path::filename(path).map_or("".into(), |name| name.to_string())) } else { @@ -84,7 +84,7 @@ impl App let entries = filesystem.ls(&selected_dir)?; let entries = entries .into_iter() - .map(|entry| path_diff(&entry, &selected_dir).to_string()) + .map(|entry| path::diff(&entry, &selected_dir).to_string()) .collect::>(); let entries = entries @@ -93,7 +93,7 @@ impl App for entry in entries { - if let Ok(result) = PathResult::new(&path_join(&selected_dir, &entry, filesystem.separator()), filesystem) + if let Ok(result) = PathResult::new(&path::join(&selected_dir, &entry, filesystem.separator()), filesystem) { ret.push(result); } diff --git a/src/app/files/filesystem.rs b/src/app/files/filesystem.rs index 5cd1527..e135772 100644 --- a/src/app/files/filesystem.rs +++ b/src/app/files/filesystem.rs @@ -2,7 +2,7 @@ use std::{error::Error, path::Path}; use crate::app::ssh::connection::Connection; -use super::str_path::{path_join, path_parent}; +use super::path; pub enum FileSystem { @@ -71,9 +71,9 @@ impl FileSystem } Self::Remote { connection, .. } => connection.ls(path) }?; - if path_parent(path).is_some() + if path::parent(path).is_some() { - ret.insert(0,path_join(path,"..", self.separator())); + ret.insert(0,path::join(path,"..", self.separator())); } Ok(ret) } diff --git a/src/app/files/mod.rs b/src/app/files/mod.rs index b1bdcd4..5578825 100644 --- a/src/app/files/mod.rs +++ b/src/app/files/mod.rs @@ -1,4 +1,4 @@ mod files; pub mod path_result; pub mod filesystem; -pub mod str_path; \ No newline at end of file +pub mod path; \ No newline at end of file diff --git a/src/app/files/path.rs b/src/app/files/path.rs new file mode 100644 index 0000000..ca680e4 --- /dev/null +++ b/src/app/files/path.rs @@ -0,0 +1,198 @@ +use regex::Regex; + +pub fn is_absolute(path: &str) -> bool +{ + path.starts_with('/') || + Regex::new(r"(^(\\\\\?\\)?[a-zA-Z]:)|(^\\\\\?\\[a-zA-Z]{1,})") + .expect("Invalid regex.") + .is_match(path) +} + +pub fn is_root(path: &str) -> bool +{ + path == "/" || + Regex::new(r"(^(\\\\\?\\)?[a-zA-Z]:\\?$)|(^\\\\\?\\[a-zA-Z]{1,}\\?$)") + .expect("Invalid regex.") + .is_match(path) +} + +pub fn parent(path: &str) -> Option<&str> +{ + if is_root(path) + { + return None; + } + let path = path.trim_end_matches(|c| c == '/' || c == '\\'); + let last_delimiter_index = path.rfind(|c| c == '/' || c == '\\'); + Some(path.split_at(last_delimiter_index? + 1).0) +} + +pub fn join(start: &str, end: &str, separator: char) -> String +{ + let start = start.trim_end_matches(|c| c == separator); + let end = end.trim_start_matches(|c| c == separator); + if end == ".." + { + parent(start).unwrap_or(start).to_string() + } + else if end == "." + { + start.to_string() + } + else + { + format!("{}{}{}", start, separator, end) + } +} + +pub fn filename(path: &str) -> Option<&str> +{ + if is_root(path) + { + None + } + else + { + let path = path.trim_end_matches(|c| c == '/' || c == '\\'); + let path = path.rsplit(|c| c == '/' || c == '\\').collect::>(); + Some(path[0]) + } +} + +pub fn diff<'a>(full_path: &'a str, prefix_path: &str) -> &'a str +{ + if full_path == prefix_path + { + return "."; + } + else if let Some(unprefixed_path) = full_path.strip_prefix(prefix_path) + { + unprefixed_path.trim_start_matches(|c| c == '/' || c == '\\') + } + else if let Some(unprefixed_path_reverse_logic) = prefix_path.strip_prefix(full_path) + { + if unprefixed_path_reverse_logic.split(|c| c == '/' || c == '\\').filter(|s|!s.is_empty()).count() == 1 + { + return ".."; + } + else + { + full_path + } + } + else + { + full_path + } +} + +#[cfg(test)] +mod test +{ + use super::*; + + #[test] + fn test_is_absolute() + { + assert!(is_absolute("/")); + assert!(!is_absolute("test")); + assert!(is_absolute("/home/user")); + assert!(!is_absolute("home/user")); + + assert!(!is_absolute("\\\\?\\")); + assert!(is_absolute("\\\\?\\C:\\Users\\")); + assert!(is_absolute("\\\\?\\d:\\Users")); + assert!(is_absolute("C:\\Users")); + assert!(is_absolute("d:\\Users")); + assert!(!is_absolute("ASDF\\Users")); + assert!(is_absolute("\\\\?\\ASDF\\")); + assert!(!is_absolute(".\\Users")); + } + + #[test] + fn test_is_root() + { + assert!(is_root("/")); + assert!(!is_root("test")); + assert!(!is_root("/home/user")); + assert!(!is_root("home/user")); + + assert!(!is_root("\\\\?\\")); + assert!(is_root("\\\\?\\C:\\")); + assert!(is_root("\\\\?\\d:")); + assert!(is_root("C:\\")); + assert!(is_root("d:")); + assert!(!is_root("ASDF\\")); + assert!(is_root("\\\\?\\ASDF\\")); + assert!(!is_root(".\\Users")); + } + + #[test] + fn test_parent() + { + assert_eq!(parent("/home/user"), Some("/home/")); + assert_eq!(parent("/home"), Some("/")); + assert_eq!(parent("/"), None); + assert_eq!(parent("C:\\Users\\user"), Some("C:\\Users\\")); + assert_eq!(parent("C:\\Users"), Some("C:\\")); + assert_eq!(parent("C:\\"), None); + assert_eq!(parent("\\\\?\\C:\\Users\\user"), Some("\\\\?\\C:\\Users\\")); + assert_eq!(parent("\\\\?\\C:\\Users"), Some("\\\\?\\C:\\")); + assert_eq!(parent("\\\\?\\C:\\"), None); + assert_eq!(parent("\\\\?\\ASDF\\Users\\user"), Some("\\\\?\\ASDF\\Users\\")); + assert_eq!(parent("\\\\?\\ASDF\\Users"), Some("\\\\?\\ASDF\\")); + assert_eq!(parent("\\\\?\\ASDF"), None); + } + + #[test] + fn test_join() + { + assert_eq!(join("/home", "user", '/'), "/home/user"); + assert_eq!(join("/home", "..", '/'), "/"); + assert_eq!(join("/home", ".", '/'), "/home"); + + assert_eq!(join("C:\\Users", "user", '\\'), "C:\\Users\\user"); + assert_eq!(join("C:\\Users", "..", '\\'), "C:\\"); + assert_eq!(join("C:\\Users", ".", '\\'), "C:\\Users"); + } + + #[test] + fn test_filename() + { + assert_eq!(filename("/home/user"), Some("user")); + assert_eq!(filename("/home"), Some("home")); + assert_eq!(filename("/"), None); + assert_eq!(filename("C:\\Users\\user"), Some("user")); + assert_eq!(filename("C:\\Users"), Some("Users")); + assert_eq!(filename("C:\\"), None); + assert_eq!(filename("\\\\?\\C:\\Users\\user"), Some("user")); + assert_eq!(filename("\\\\?\\C:\\Users"), Some("Users")); + assert_eq!(filename("\\\\?\\C:\\"), None); + assert_eq!(filename("\\\\?\\ASDF\\Users\\user"), Some("user")); + assert_eq!(filename("\\\\?\\ASDF\\Users"), Some("Users")); + assert_eq!(filename("\\\\?\\ASDF"), None); + } + + #[test] + fn test_diff() + { + assert_eq!(diff("/home/user", "/home"), "user"); + assert_eq!(diff("/home", "/home/user"), ".."); + assert_eq!(diff("/home", "/home"), "."); + assert_eq!(diff("/home", "/"), "home"); + assert_eq!(diff("/", "/"), "."); + assert_eq!(diff("C:\\Users\\user", "C:\\Users"), "user"); + assert_eq!(diff("C:\\Users", "C:\\Users\\user"), ".."); + assert_eq!(diff("C:\\Users", "C:\\Users"), "."); + assert_eq!(diff("C:\\Users", "C:\\"), "Users"); + assert_eq!(diff("C:\\", "C:\\"), "."); + assert_eq!(diff("\\\\?\\C:\\Users\\user", "\\\\?\\C:\\Users"), "user"); + assert_eq!(diff("\\\\?\\C:\\Users", "\\\\?\\C:\\Users"), "."); + assert_eq!(diff("\\\\?\\C:\\Users", "\\\\?\\C:\\"), "Users"); + assert_eq!(diff("\\\\?\\C:\\", "\\\\?\\C:\\"), "."); + assert_eq!(diff("\\\\?\\ASDF\\Users\\user", "\\\\?\\ASDF\\Users"), "user"); + assert_eq!(diff("\\\\?\\ASDF\\Users", "\\\\?\\ASDF\\Users"), "."); + assert_eq!(diff("\\\\?\\ASDF\\Users", "\\\\?\\ASDF"), "Users"); + assert_eq!(diff("\\\\?\\ASDF", "\\\\?\\ASDF"), "."); + } +} \ No newline at end of file diff --git a/src/app/files/path_result.rs b/src/app/files/path_result.rs index 9104461..9e72bdf 100644 --- a/src/app/files/path_result.rs +++ b/src/app/files/path_result.rs @@ -4,7 +4,7 @@ use ratatui::text::{Line, Span}; use crate::app::settings::color_settings::ColorSettings; -use super::{filesystem::FileSystem, str_path::path_diff}; +use super::{filesystem::FileSystem, path}; #[derive(Debug, Clone, PartialEq, Eq)] pub struct PathResult @@ -51,7 +51,7 @@ impl PathResult { color_settings.path_file }; - let path = path_diff(&self.path, base_path); + let path = path::diff(&self.path, base_path); ret.spans.push(Span::styled(path.to_string(), style)); ret.left_aligned() diff --git a/src/app/files/str_path.rs b/src/app/files/str_path.rs deleted file mode 100644 index f700049..0000000 --- a/src/app/files/str_path.rs +++ /dev/null @@ -1,79 +0,0 @@ -pub fn path_is_absolute(path: &str) -> bool -{ - path.starts_with('/') || path.starts_with('\\') -} - -pub fn path_is_root(path: &str) -> bool -{ - path == "/" || - (path.strip_prefix("\\\\?\\").map( - |path| - { - let bs_count = path.chars().filter(|c| *c=='\\').count(); - (bs_count == 1 && path.ends_with('\\')) || - (bs_count == 0) - } - ).unwrap_or(false)) -} - -pub fn path_parent(path: &str) -> Option<&str> -{ - if path_is_root(path) - { - return None; - } - let path = path.trim_end_matches(|c| c == '/' || c == '\\'); - let last_delimiter_index = path.rfind(|c| c == '/' || c == '\\'); - Some(path.split_at(last_delimiter_index? + 1).0) -} - -pub fn path_join(start: &str, end: &str, separator: char) -> String -{ - let start = start.trim_end_matches(|c| c == separator); - let end = end.trim_start_matches(|c| c == separator); - if end == ".." - { - path_parent(start).unwrap_or(start).to_string() - } - else - { - format!("{}{}{}", start, separator, end) - } -} - -pub fn path_filename(path: &str) -> Option<&str> -{ - if path_is_root(path) - { - None - } - else - { - let path = path.trim_end_matches(|c| c == '/' || c == '\\'); - let path = path.rsplit(|c| c == '/' || c == '\\').collect::>(); - Some(path[0]) - } -} - -pub fn path_diff<'a>(full_path: &'a str, prefix_path: &str) -> &'a str -{ - if let Some(unprefixed_path) = full_path.strip_prefix(prefix_path) - { - unprefixed_path.trim_start_matches(|c| c == '/' || c == '\\') - } - else if let Some(unprefixed_path_reverse_logic) = prefix_path.strip_prefix(full_path) - { - if unprefixed_path_reverse_logic.split(|c| c == '/' || c == '\\').filter(|s|!s.is_empty()).count() == 1 - { - return ".."; - } - else - { - full_path - } - } - else - { - full_path - } -} \ No newline at end of file diff --git a/src/app/hex.rs b/src/app/hex.rs index ec1c544..804cde0 100644 --- a/src/app/hex.rs +++ b/src/app/hex.rs @@ -2,7 +2,7 @@ use std::error::Error; use ratatui::text::{Line, Span, Text}; -use super::{assembly::AssemblyLine, files::{filesystem::FileSystem, str_path::path_parent}, info_mode::InfoMode, notification::NotificationLevel, settings::color_settings::ColorSettings, App}; +use super::{assembly::AssemblyLine, files::{filesystem::FileSystem, path}, info_mode::InfoMode, notification::NotificationLevel, settings::color_settings::ColorSettings, App}; pub(super) struct InstructionInfo { @@ -211,7 +211,7 @@ impl App pub(super) fn save_as(&mut self, path: &str) -> Result<(), Box> { - if let Some(parent) = path_parent(path) + if let Some(parent) = path::parent(path) { self.filesystem.mkdirs(parent)?; }; diff --git a/src/app/popup_state.rs b/src/app/popup_state.rs index 063c937..e5c74a4 100644 --- a/src/app/popup_state.rs +++ b/src/app/popup_state.rs @@ -2,7 +2,7 @@ use std::error::Error; use ratatui::text::{Line, Span, Text}; -use super::{assembly::AssemblyLine, files::{path_result::PathResult, str_path::{path_diff, path_parent}}, run_command::Command, settings::color_settings::ColorSettings, App}; +use super::{assembly::AssemblyLine, files::{path_result::PathResult, path}, run_command::Command, settings::color_settings::ColorSettings, App}; #[derive(Clone, Debug)] pub enum PopupState @@ -309,11 +309,11 @@ impl App let editable_string = Self::get_line_from_string_and_cursor(color_settings, path, *cursor, "Path", available_width, true); - let (prefix, currently_open_path_text) = if let Some(parent) = path_parent(currently_open_path) + let (prefix, currently_open_path_text) = if let Some(parent) = path::parent(currently_open_path) { ( ".../", - path_diff(currently_open_path, parent) + path::diff(currently_open_path, parent) ) } else diff --git a/src/app/ssh/connection.rs b/src/app/ssh/connection.rs index 81e6996..980f1c9 100644 --- a/src/app/ssh/connection.rs +++ b/src/app/ssh/connection.rs @@ -3,7 +3,7 @@ use std::{error::Error, fmt::Display, path::PathBuf}; use russh::client::{self, Handler}; use russh_sftp::client::SftpSession; -use crate::app::files::str_path::{path_join, path_parent}; +use crate::app::files::path; pub struct SSHClient; impl Handler for SSHClient @@ -140,7 +140,7 @@ impl Connection self.runtime.block_on(async { let mut paths = vec![path]; let mut current = path; - while let Some(parent) = path_parent(current) + while let Some(parent) = path::parent(current) { paths.push(parent); current = parent; @@ -172,7 +172,7 @@ impl Connection { let dir = self.runtime.block_on(self.sftp.read_dir(path))?; dir.into_iter().map(|entry| { - Ok(path_join(path,&entry.file_name(), self.separator()).to_string()) + Ok(path::join(path,&entry.file_name(), self.separator()).to_string()) }).collect() } diff --git a/src/headers/generic.rs b/src/headers/generic.rs index 9d80456..446986c 100644 --- a/src/headers/generic.rs +++ b/src/headers/generic.rs @@ -3,7 +3,7 @@ use std::{collections::HashMap, io::Write}; use object::{Object, ObjectSection, ObjectSymbol}; use pdb::FallibleIterator; -use crate::app::files::{filesystem::FileSystem, str_path::{path_is_absolute, path_join, path_parent}}; +use crate::app::files::{filesystem::FileSystem, path}; use super::header::Section; @@ -143,13 +143,13 @@ impl GenericHeader } if let Some(pdb_file_path) = pdb_file_path { - let pdb_absolute_path = if path_is_absolute(&pdb_file_path) + let pdb_absolute_path = if path::is_absolute(&pdb_file_path) { pdb_file_path } else { - path_join(path_parent(file_path).unwrap_or("./"), &pdb_file_path, filesystem.separator()) + path::join(path::parent(file_path).unwrap_or("./"), &pdb_file_path, filesystem.separator()) }; let file = filesystem.read(&pdb_absolute_path); if let Ok(file) = file