//! ## 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) } }