diff --git a/src/fs/explorer/formatter.rs b/src/fs/explorer/formatter.rs new file mode 100755 index 0000000..8915dc8 --- /dev/null +++ b/src/fs/explorer/formatter.rs @@ -0,0 +1,710 @@ +//! ## Formatter +//! +//! `formatter` is the module which provides formatting utilities for `FileExplorer` + +/* +* +* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com +* +* This file is part of "TermSCP" +* +* TermSCP is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* TermSCP is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with TermSCP. If not, see . +* +*/ + +// Deps +extern crate bytesize; +extern crate regex; +#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))] +extern crate users; +// Locals +use super::FsEntry; +use crate::utils::fmt::{fmt_path_elide, fmt_pex, fmt_time}; +// Ext +use bytesize::ByteSize; +use regex::Regex; +#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))] +use users::{get_group_by_gid, get_user_by_uid}; +// Types +type FmtCallback = fn(&Formatter, &FsEntry, &str) -> String; + +// Keys +const FMT_KEY_ATIME: &str = "{ATIME}"; +const FMT_KEY_CTIME: &str = "{CTIME}"; +const FMT_KEY_GROUP: &str = "{GROUP}"; +const FMT_KEY_MTIME: &str = "{MTIME}"; +const FMT_KEY_NAME: &str = "{NAME}"; +const FMT_KEY_PEX: &str = "{PEX}"; +const FMT_KEY_SIZE: &str = "{SIZE}"; +const FMT_KEY_SYMLINK: &str = "{SYMLINK}"; +const FMT_KEY_USER: &str = "{USER}"; +// Default +const FMT_DEFAULT_STX: &str = "{NAME} {PEX} {USER} {SIZE} {MTIME}"; +// Regex +lazy_static! { + static ref FMT_KEY_REGEX: Regex = Regex::new(r"\{(.*?)\}").ok().unwrap(); +} + +/// ## CallChainBlock +/// +/// Call Chain block is a block in a chain of functions which are called in order to format the FsEntry. +/// A callChain is instantiated starting from the Formatter syntax and the regex, once the groups are found +/// a chain of function is made using the Formatters method. +/// This method provides an extremely fast way to format fs entries +struct CallChainBlock { + func: FmtCallback, + next_block: Option>, +} + +impl CallChainBlock { + /// ### new + /// + /// Create a new `CallChainBlock` + pub fn new(func: FmtCallback) -> Self { + CallChainBlock { + func, + next_block: None, + } + } + + /// ### next + /// + /// Call next callback in the CallChain + pub fn next(&self, fmt: &Formatter, fsentry: &FsEntry, cur_str: &str) -> String { + // Call func + let new_str: String = (self.func)(fmt, fsentry, cur_str); + // If next is some, call next, otherwise (END OF CHAIN) return new_str + match &self.next_block { + Some(block) => block.next(fmt, fsentry, new_str.as_str()), + None => new_str, + } + } + + /// ### push + /// + /// Push func to the last element in the Call chain + pub fn push(&mut self, func: FmtCallback) { + // Call recursively until an element with next_block equal to None is found + match &mut self.next_block { + None => self.next_block = Some(Box::new(CallChainBlock::new(func))), + Some(block) => block.push(func), + } + } +} + +/// ## Formatter +/// +/// Formatter takes care of formatting FsEntries according to the provided keys. +/// Formatting is performed using the `CallChainBlock`, which composed makes a Call Chain. This method is extremely fast compared to match the format groups +/// at each fmt call. +pub struct Formatter { + fmt_str: String, + call_chain: CallChainBlock, +} + +impl Default for Formatter { + /// ### default + /// + /// Instantiates a Formatter with the default fmt syntax + fn default() -> Self { + Formatter { + fmt_str: FMT_DEFAULT_STX.to_string(), + call_chain: Self::make_callchain(FMT_DEFAULT_STX), + } + } +} + +impl Formatter { + /// ### new + /// + /// Instantiates a new `Formatter` with the provided format string + pub fn new(fmt_str: &str) -> Self { + Formatter { + fmt_str: fmt_str.to_string(), + call_chain: Self::make_callchain(fmt_str), + } + } + + /// ### fmt + /// + /// Format fsentry + pub fn fmt(&self, fsentry: &FsEntry) -> String { + // Execute callchain blocks + self.call_chain.next(self, fsentry, self.fmt_str.as_str()) + } + + // Fmt methods + + /// ### fmt_atime + /// + /// Format last access time + fn fmt_atime(&self, fsentry: &FsEntry, cur_str: &str) -> String { + // Get date + let datetime: String = fmt_time(fsentry.get_last_access_time(), "%b %d %Y %H:%M"); + // Replace `FMT_KEY_ATIME` with datetime + cur_str.replace(FMT_KEY_ATIME, format!("{:17}", datetime).as_str()) + } + + /// ### fmt_ctime + /// + /// Format creation time + fn fmt_ctime(&self, fsentry: &FsEntry, cur_str: &str) -> String { + // Get date + let datetime: String = fmt_time(fsentry.get_creation_time(), "%b %d %Y %H:%M"); + // Replace `FMT_KEY_ATIME` with datetime + cur_str.replace(FMT_KEY_CTIME, format!("{:17}", datetime).as_str()) + } + + /// ### fmt_group + /// + /// Format owner group + fn fmt_group(&self, fsentry: &FsEntry, cur_str: &str) -> String { + // Get username + #[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))] + let group: String = match fsentry.get_group() { + Some(gid) => match get_group_by_gid(gid) { + Some(user) => user.name().to_string_lossy().to_string(), + None => gid.to_string(), + }, + None => 0.to_string(), + }; + #[cfg(target_os = "windows")] + let group: String = match fsentry.get_group() { + Some(gid) => gid.to_string(), + None => 0.to_string(), + }; + // Replace `FMT_KEY_GROUP` with size + cur_str.replace(FMT_KEY_GROUP, format!("{:12}", group).as_str()) + } + + /// ### fmt_mtime + /// + /// Format last change time + fn fmt_mtime(&self, fsentry: &FsEntry, cur_str: &str) -> String { + // Get date + let datetime: String = fmt_time(fsentry.get_last_change_time(), "%b %d %Y %H:%M"); + // Replace `FMT_KEY_MTIME` with datetime + cur_str.replace(FMT_KEY_MTIME, format!("{:17}", datetime).as_str()) + } + + /// ### fmt_name + /// + /// Format file name + fn fmt_name(&self, fsentry: &FsEntry, cur_str: &str) -> String { + // Get file name (or elide if too long) + let name: &str = fsentry.get_name(); + let last_idx: usize = match fsentry.is_dir() { + // NOTE: For directories is 19, since we push '/' to name + true => 19, + false => 20, + }; + let mut name: String = match name.len() >= 24 { + false => name.to_string(), + true => format!("{}...", &name[0..last_idx]), + }; + if fsentry.is_dir() { + name.push('/'); + } + // Replace `FMT_KEY_NAME` with name + cur_str.replace(FMT_KEY_NAME, format!("{:24}", name).as_str()) + } + + /// ### fmt_pex + /// + /// Format file permissions + fn fmt_pex(&self, fsentry: &FsEntry, cur_str: &str) -> String { + // Create mode string + let mut pex: String = String::with_capacity(10); + let file_type: char = match fsentry.is_symlink() { + true => 'l', + false => match fsentry.is_dir() { + true => 'd', + false => '-', + }, + }; + pex.push(file_type); + match fsentry.get_unix_pex() { + None => pex.push_str("?????????"), + Some((owner, group, others)) => pex.push_str(fmt_pex(owner, group, others).as_str()), + } + // Replace `FMT_KEY_PEX` with pex + cur_str.replace(FMT_KEY_PEX, format!("{:10}", pex).as_str()) + } + + /// ### fmt_size + /// + /// Format file size + fn fmt_size(&self, fsentry: &FsEntry, cur_str: &str) -> String { + if fsentry.is_file() { + // Get byte size + let size: ByteSize = ByteSize(fsentry.get_size() as u64); + // Replace `FMT_KEY_SIZE` with size + cur_str.replace(FMT_KEY_SIZE, format!("{:10}", size.to_string()).as_str()) + } else { + // No size for directories + cur_str.replace(FMT_KEY_SIZE, " ") + } + } + + /// ### fmt_symlink + /// + /// Format file symlink (if any) + fn fmt_symlink(&self, fsentry: &FsEntry, cur_str: &str) -> String { + // Get file name (or elide if too long) + // Replace `FMT_KEY_NAME` with name + match fsentry.is_symlink() { + false => cur_str.replace(FMT_KEY_SYMLINK, " "), + true => cur_str.replace( + FMT_KEY_SYMLINK, + format!( + "-> {:21}", + fmt_path_elide(fsentry.get_realfile().get_abs_path().as_path(), 20) + ) + .as_str(), + ), + } + } + + /// ### fmt_user + /// + /// Format owner user + fn fmt_user(&self, fsentry: &FsEntry, cur_str: &str) -> String { + // Get username + #[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))] + let username: String = match fsentry.get_user() { + Some(uid) => match get_user_by_uid(uid) { + Some(user) => user.name().to_string_lossy().to_string(), + None => uid.to_string(), + }, + None => 0.to_string(), + }; + #[cfg(target_os = "windows")] + let username: String = match fsentry.get_user() { + Some(uid) => uid.to_string(), + None => 0.to_string(), + }; + // Replace `FMT_KEY_USER` with size + cur_str.replace(FMT_KEY_USER, format!("{:12}", username).as_str()) + } + + /// ### fmt_fallback + /// + /// Fallback function in case the format key is unknown + /// It does nothing, just returns cur_str + fn fmt_fallback(&self, _fsentry: &FsEntry, cur_str: &str) -> String { + cur_str.to_string() + } + + // Static + + /// ### make_callchain + /// + /// Make a callchain starting from the fmt str + fn make_callchain(fmt_str: &str) -> CallChainBlock { + // Init chain block + let mut callchain: Option = None; + // Match fmt str against regex + for regex_match in FMT_KEY_REGEX.captures_iter(fmt_str) { + // Match the match (I guess...) + let callback: FmtCallback = match ®ex_match[0] { + FMT_KEY_ATIME => Self::fmt_atime, + FMT_KEY_CTIME => Self::fmt_ctime, + FMT_KEY_GROUP => Self::fmt_group, + FMT_KEY_MTIME => Self::fmt_mtime, + FMT_KEY_NAME => Self::fmt_name, + FMT_KEY_PEX => Self::fmt_pex, + FMT_KEY_SIZE => Self::fmt_size, + FMT_KEY_SYMLINK => Self::fmt_symlink, + FMT_KEY_USER => Self::fmt_user, + _ => Self::fmt_fallback, + }; + // Create a callchain or push new element to its back + match callchain.as_mut() { + None => callchain = Some(CallChainBlock::new(callback)), + Some(chain_block) => chain_block.push(callback), + } + } + // Finalize and return + match callchain { + Some(callchain) => callchain, + None => CallChainBlock::new(Self::fmt_fallback), + } + } +} + +#[cfg(test)] +mod tests { + + use super::*; + use crate::fs::{FsDirectory, FsFile}; + use std::path::PathBuf; + use std::time::SystemTime; + + #[test] + fn test_fs_explorer_formatter_callchain() { + // Make a dummy formatter + let dummy_formatter: Formatter = Formatter::new(""); + // Make a dummy entry + let t_now: SystemTime = SystemTime::now(); + let dummy_entry: FsEntry = FsEntry::File(FsFile { + name: String::from("bar.txt"), + abs_path: PathBuf::from("/bar.txt"), + last_change_time: t_now, + last_access_time: t_now, + creation_time: t_now, + size: 8192, + readonly: false, + ftype: Some(String::from("txt")), + symlink: None, // UNIX only + user: Some(0), // UNIX only + group: Some(0), // UNIX only + unix_pex: Some((6, 4, 4)), // UNIX only + }); + let mut callchain: CallChainBlock = CallChainBlock::new(dummy_fmt); + assert!(callchain.next_block.is_none()); + // Execute + assert_eq!( + callchain.next(&dummy_formatter, &dummy_entry, ""), + String::from("A") + ); + // Push 4 new blocks + callchain.push(dummy_fmt); + callchain.push(dummy_fmt); + callchain.push(dummy_fmt); + callchain.push(dummy_fmt); + // Verify + assert_eq!( + callchain.next(&dummy_formatter, &dummy_entry, ""), + String::from("AAAAA") + ); + } + + #[test] + fn test_fs_explorer_formatter_format_files() { + // Make default + let formatter: Formatter = Formatter::default(); + // Experiments :D + let t: SystemTime = SystemTime::now(); + let entry: FsEntry = FsEntry::File(FsFile { + name: String::from("bar.txt"), + abs_path: PathBuf::from("/bar.txt"), + last_change_time: t, + last_access_time: t, + creation_time: t, + size: 8192, + readonly: false, + ftype: Some(String::from("txt")), + symlink: None, // UNIX only + user: Some(0), // UNIX only + group: Some(0), // UNIX only + unix_pex: Some((6, 4, 4)), // UNIX only + }); + #[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))] + assert_eq!( + formatter.fmt(&entry), + format!( + "bar.txt -rw-r--r-- root 8.2 KB {}", + fmt_time(t, "%b %d %Y %H:%M") + ) + ); + #[cfg(target_os = "windows")] + assert_eq!( + formatter.fmt(&entry), + format!( + "bar.txt -rw-r--r-- 0 8.2 KB {}", + fmt_time(t, "%b %d %Y %H:%M") + ) + ); + // Elide name + let entry: FsEntry = FsEntry::File(FsFile { + name: String::from("piroparoporoperoperupupu.txt"), + abs_path: PathBuf::from("/bar.txt"), + last_change_time: t, + last_access_time: t, + creation_time: t, + size: 8192, + readonly: false, + ftype: Some(String::from("txt")), + symlink: None, // UNIX only + user: Some(0), // UNIX only + group: Some(0), // UNIX only + unix_pex: Some((6, 4, 4)), // UNIX only + }); + #[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))] + assert_eq!( + formatter.fmt(&entry), + format!( + "piroparoporoperoperu... -rw-r--r-- root 8.2 KB {}", + fmt_time(t, "%b %d %Y %H:%M") + ) + ); + #[cfg(target_os = "windows")] + assert_eq!( + formatter.fmt(&entry), + format!( + "piroparoporoperoperu... -rw-r--r-- 0 8.2 KB {}", + fmt_time(t, "%b %d %Y %H:%M") + ) + ); + // No pex + let entry: FsEntry = FsEntry::File(FsFile { + name: String::from("bar.txt"), + abs_path: PathBuf::from("/bar.txt"), + last_change_time: t, + last_access_time: t, + creation_time: t, + size: 8192, + readonly: false, + ftype: Some(String::from("txt")), + symlink: None, // UNIX only + user: Some(0), // UNIX only + group: Some(0), // UNIX only + unix_pex: None, // UNIX only + }); + #[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))] + assert_eq!( + formatter.fmt(&entry), + format!( + "bar.txt -????????? root 8.2 KB {}", + fmt_time(t, "%b %d %Y %H:%M") + ) + ); + #[cfg(target_os = "windows")] + assert_eq!( + formatter.fmt(&entry), + format!( + "bar.txt -????????? 0 8.2 KB {}", + fmt_time(t, "%b %d %Y %H:%M") + ) + ); + // No user + let entry: FsEntry = FsEntry::File(FsFile { + name: String::from("bar.txt"), + abs_path: PathBuf::from("/bar.txt"), + last_change_time: t, + last_access_time: t, + creation_time: t, + size: 8192, + readonly: false, + ftype: Some(String::from("txt")), + symlink: None, // UNIX only + user: None, // UNIX only + group: Some(0), // UNIX only + unix_pex: None, // UNIX only + }); + #[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))] + assert_eq!( + formatter.fmt(&entry), + format!( + "bar.txt -????????? 0 8.2 KB {}", + fmt_time(t, "%b %d %Y %H:%M") + ) + ); + #[cfg(target_os = "windows")] + assert_eq!( + formatter.fmt(&entry), + format!( + "bar.txt -????????? 0 8.2 KB {}", + fmt_time(t, "%b %d %Y %H:%M") + ) + ); + } + + #[test] + fn test_fs_explorer_formatter_format_dirs() { + // Make default + let formatter: Formatter = Formatter::default(); + // Experiments :D + let t_now: SystemTime = SystemTime::now(); + let entry: FsEntry = FsEntry::Directory(FsDirectory { + name: String::from("projects"), + abs_path: PathBuf::from("/home/cvisintin/projects"), + last_change_time: t_now, + last_access_time: t_now, + creation_time: t_now, + readonly: false, + symlink: None, // UNIX only + user: Some(0), // UNIX only + group: Some(0), // UNIX only + unix_pex: Some((7, 5, 5)), // UNIX only + }); + #[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))] + assert_eq!( + formatter.fmt(&entry), + format!( + "projects/ drwxr-xr-x root {}", + fmt_time(t_now, "%b %d %Y %H:%M") + ) + ); + #[cfg(target_os = "windows")] + assert_eq!( + formatter.fmt(&entry), + format!( + "projects/ drwxr-xr-x 0 {}", + fmt_time(t_now, "%b %d %Y %H:%M") + ) + ); + // No pex, no user + let entry: FsEntry = FsEntry::Directory(FsDirectory { + name: String::from("projects"), + abs_path: PathBuf::from("/home/cvisintin/projects"), + last_change_time: t_now, + last_access_time: t_now, + creation_time: t_now, + readonly: false, + symlink: None, // UNIX only + user: None, // UNIX only + group: Some(0), // UNIX only + unix_pex: None, // UNIX only + }); + #[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))] + assert_eq!( + formatter.fmt(&entry), + format!( + "projects/ d????????? 0 {}", + fmt_time(t_now, "%b %d %Y %H:%M") + ) + ); + #[cfg(target_os = "windows")] + assert_eq!( + formatter.fmt(&entry), + format!( + "projects/ d????????? 0 {}", + fmt_time(t_now, "%b %d %Y %H:%M") + ) + ); + } + + #[test] + fn test_fs_explorer_formatter_all_together_now() { + let formatter: Formatter = + Formatter::new("{NAME} {SYMLINK} {GROUP} {USER} {PEX} {SIZE} {ATIME} {CTIME} {MTIME}"); + // Directory (with symlink) + let t: SystemTime = SystemTime::now(); + let pointer: FsEntry = FsEntry::File(FsFile { + name: String::from("project.info"), + abs_path: PathBuf::from("/project.info"), + last_change_time: t, + last_access_time: t, + creation_time: t, + size: 8192, + readonly: false, + ftype: Some(String::from("txt")), + symlink: None, // UNIX only + user: None, // UNIX only + group: None, // UNIX only + unix_pex: None, // UNIX only + }); + let entry: FsEntry = FsEntry::Directory(FsDirectory { + name: String::from("projects"), + abs_path: PathBuf::from("/home/cvisintin/project"), + last_change_time: t, + last_access_time: t, + creation_time: t, + readonly: false, + symlink: Some(Box::new(pointer)), // UNIX only + user: None, // UNIX only + group: None, // UNIX only + unix_pex: Some((7, 5, 5)), // UNIX only + }); + assert_eq!(formatter.fmt(&entry), format!( + "projects/ -> /project.info 0 0 lrwxr-xr-x {} {} {}", + fmt_time(t, "%b %d %Y %H:%M"), + fmt_time(t, "%b %d %Y %H:%M"), + fmt_time(t, "%b %d %Y %H:%M"), + )); + // Directory without symlink + let entry: FsEntry = FsEntry::Directory(FsDirectory { + name: String::from("projects"), + abs_path: PathBuf::from("/home/cvisintin/project"), + last_change_time: t, + last_access_time: t, + creation_time: t, + readonly: false, + symlink: None, // UNIX only + user: None, // UNIX only + group: None, // UNIX only + unix_pex: Some((7, 5, 5)), // UNIX only + }); + assert_eq!(formatter.fmt(&entry), format!( + "projects/ 0 0 drwxr-xr-x {} {} {}", + fmt_time(t, "%b %d %Y %H:%M"), + fmt_time(t, "%b %d %Y %H:%M"), + fmt_time(t, "%b %d %Y %H:%M"), + )); + // File with symlink + let pointer: FsEntry = FsEntry::File(FsFile { + name: String::from("project.info"), + abs_path: PathBuf::from("/project.info"), + last_change_time: t, + last_access_time: t, + creation_time: t, + size: 8192, + readonly: false, + ftype: Some(String::from("txt")), + symlink: None, // UNIX only + user: None, // UNIX only + group: None, // UNIX only + unix_pex: None, // UNIX only + }); + let entry: FsEntry = FsEntry::File(FsFile { + name: String::from("bar.txt"), + abs_path: PathBuf::from("/bar.txt"), + last_change_time: t, + last_access_time: t, + creation_time: t, + size: 8192, + readonly: false, + ftype: Some(String::from("txt")), + symlink: Some(Box::new(pointer)), // UNIX only + user: None, // UNIX only + group: None, // UNIX only + unix_pex: Some((6, 4, 4)), // UNIX only + }); + assert_eq!(formatter.fmt(&entry), format!( + "bar.txt -> /project.info 0 0 lrw-r--r-- 8.2 KB {} {} {}", + fmt_time(t, "%b %d %Y %H:%M"), + fmt_time(t, "%b %d %Y %H:%M"), + fmt_time(t, "%b %d %Y %H:%M"), + )); + // File without symlink + let entry: FsEntry = FsEntry::File(FsFile { + name: String::from("bar.txt"), + abs_path: PathBuf::from("/bar.txt"), + last_change_time: t, + last_access_time: t, + creation_time: t, + size: 8192, + readonly: false, + ftype: Some(String::from("txt")), + symlink: None, // UNIX only + user: None, // UNIX only + group: None, // UNIX only + unix_pex: Some((6, 4, 4)), // UNIX only + }); + assert_eq!(formatter.fmt(&entry), format!( + "bar.txt 0 0 -rw-r--r-- 8.2 KB {} {} {}", + fmt_time(t, "%b %d %Y %H:%M"), + fmt_time(t, "%b %d %Y %H:%M"), + fmt_time(t, "%b %d %Y %H:%M"), + )); + } + + /// ### dummy_fmt + /// + /// Dummy formatter, just yelds an 'A' at the end of the current string + fn dummy_fmt(_fmt: &Formatter, _entry: &FsEntry, cur_str: &str) -> String { + format!("{}A", cur_str) + } +} diff --git a/src/fs/explorer/mod.rs b/src/fs/explorer/mod.rs index ffc8ea3..e58d612 100644 --- a/src/fs/explorer/mod.rs +++ b/src/fs/explorer/mod.rs @@ -25,10 +25,12 @@ // Mods pub(crate) mod builder; +mod formatter; // Deps extern crate bitflags; // Locals use super::FsEntry; +use formatter::Formatter; // Ext use std::cmp::Reverse; use std::collections::VecDeque;