Explorer formatter module

This commit is contained in:
veeso
2021-01-23 15:41:07 +01:00
parent 54ab24fc0c
commit c1f6308795
2 changed files with 712 additions and 0 deletions

710
src/fs/explorer/formatter.rs Executable file
View File

@@ -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 <http://www.gnu.org/licenses/>.
*
*/
// 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<Box<CallChainBlock>>,
}
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<CallChainBlock> = 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 &regex_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)
}
}

View File

@@ -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;