use std::cmp::Ordering;
use std::iter::FromIterator;
#[cfg(unix)]
use std::os::unix::fs::MetadataExt;
use crate::fs::DotFilter;
use crate::fs::File;
#[derive(PartialEq, Eq, Debug, Clone)]
pub struct FileFilter {
pub group_dirs: GroupDirs,
pub sort_field: SortField,
pub sort_by_total_size: bool,
pub reverse: bool,
pub only_dirs: bool,
pub only_files: bool,
pub dot_filter: DotFilter,
pub ignore_patterns: IgnorePatterns,
pub prune_patterns: IgnorePatterns,
pub vcs_ignore: VcsIgnore,
pub symlink_mode: SymlinkMode,
}
#[derive(PartialEq, Eq, Debug, Copy, Clone, Default)]
pub enum SymlinkMode {
#[default]
Show,
Hide,
Follow,
}
impl FileFilter {
pub fn is_pruned(&self, file: &File<'_>) -> bool {
file.is_directory() && self.prune_patterns.is_matched(&file.name)
}
pub fn filter_child_files(&self, files: &mut Vec<File<'_>>) {
files.retain(|f| ! self.ignore_patterns.is_matched(&f.name));
match self.symlink_mode {
SymlinkMode::Hide => {
files.retain(|f| ! f.is_link());
}
SymlinkMode::Follow => {
for f in files.iter_mut() {
f.deref_link();
}
}
SymlinkMode::Show => {}
}
if self.only_dirs {
files.retain(File::is_directory);
}
if self.only_files {
files.retain(|f| ! f.is_directory());
}
}
pub fn filter_argument_files(&self, files: &mut Vec<File<'_>>) {
files.retain(|f| {
! self.ignore_patterns.is_matched(&f.name)
});
if self.symlink_mode == SymlinkMode::Follow {
for f in files.iter_mut() {
f.deref_link();
}
}
}
pub fn sort_files<'a, F>(&self, files: &mut [F])
where F: AsRef<File<'a>>
{
if self.sort_by_total_size && self.sort_field == SortField::Size {
use crate::fs::fields::Size;
files.sort_by(|a, b| {
let sa = match a.as_ref().total_size() { Size::Some(s) => s, _ => 0 };
let sb = match b.as_ref().total_size() { Size::Some(s) => s, _ => 0 };
sa.cmp(&sb)
});
} else {
files.sort_by(|a, b| {
self.sort_field.compare_files(a.as_ref(), b.as_ref())
});
}
if self.reverse {
files.reverse();
}
match self.group_dirs {
GroupDirs::First => {
files.sort_by(|a, b| {
b.as_ref().points_to_directory()
.cmp(&a.as_ref().points_to_directory())
});
}
GroupDirs::Last => {
files.sort_by(|a, b| {
a.as_ref().points_to_directory()
.cmp(&b.as_ref().points_to_directory())
});
}
GroupDirs::None => {}
}
}
}
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
pub enum SortField {
Unsorted,
Name(SortCase),
Extension(SortCase),
Size,
#[cfg(unix)]
FileInode,
ModifiedDate,
AccessedDate,
ChangedDate,
CreatedDate,
FileType,
ModifiedAge,
NameMixHidden(SortCase),
}
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
pub enum SortCase {
ABCabc,
AaBbCc,
}
impl SortField {
pub fn compare_files(self, a: &File<'_>, b: &File<'_>) -> Ordering {
use self::SortCase::{ABCabc, AaBbCc};
match self {
Self::Unsorted => Ordering::Equal,
Self::Name(ABCabc) => natord::compare(&a.name, &b.name),
Self::Name(AaBbCc) => natord::compare_ignore_case(&a.name, &b.name),
Self::Size => a.metadata.len().cmp(&b.metadata.len()),
#[cfg(unix)]
Self::FileInode => a.metadata.ino().cmp(&b.metadata.ino()),
Self::ModifiedDate => a.modified_time().cmp(&b.modified_time()),
Self::AccessedDate => a.accessed_time().cmp(&b.accessed_time()),
Self::ChangedDate => a.changed_time().cmp(&b.changed_time()),
Self::CreatedDate => a.created_time().cmp(&b.created_time()),
Self::ModifiedAge => b.modified_time().cmp(&a.modified_time()),
Self::FileType => match a.type_char().cmp(&b.type_char()) { Ordering::Equal => natord::compare(&a.name, &b.name),
order => order,
},
Self::Extension(ABCabc) => match a.ext.cmp(&b.ext) {
Ordering::Equal => natord::compare(&a.name, &b.name),
order => order,
},
Self::Extension(AaBbCc) => match a.ext.cmp(&b.ext) {
Ordering::Equal => natord::compare_ignore_case(&a.name, &b.name),
order => order,
},
Self::NameMixHidden(ABCabc) => natord::compare(
Self::strip_dot(&a.name),
Self::strip_dot(&b.name)
),
Self::NameMixHidden(AaBbCc) => natord::compare_ignore_case(
Self::strip_dot(&a.name),
Self::strip_dot(&b.name)
)
}
}
fn strip_dot(n: &str) -> &str {
match n.strip_prefix('.') {
Some(s) => s,
None => n,
}
}
}
#[derive(PartialEq, Eq, Default, Debug, Clone)]
pub struct IgnorePatterns {
patterns: Vec<glob::Pattern>,
}
impl FromIterator<glob::Pattern> for IgnorePatterns {
fn from_iter<I>(iter: I) -> Self
where I: IntoIterator<Item = glob::Pattern>
{
let patterns = iter.into_iter().collect();
Self { patterns }
}
}
impl IgnorePatterns {
pub fn parse_from_iter<'a, I: IntoIterator<Item = &'a str>>(iter: I) -> (Self, Vec<glob::PatternError>) {
let iter = iter.into_iter();
let mut patterns = match iter.size_hint() {
(_, Some(count)) => Vec::with_capacity(count),
_ => Vec::new(),
};
let mut errors = Vec::new();
for input in iter {
match glob::Pattern::new(input) {
Ok(pat) => patterns.push(pat),
Err(e) => errors.push(e),
}
}
(Self { patterns }, errors)
}
pub fn empty() -> Self {
Self { patterns: Vec::new() }
}
pub fn is_matched(&self, file: &str) -> bool {
self.patterns.iter().any(|p| p.matches(file))
}
}
#[derive(PartialEq, Eq, Debug, Copy, Clone, Default)]
pub enum GroupDirs {
First,
Last,
#[default]
None,
}
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
pub enum VcsIgnore {
CheckAndIgnore,
Off,
}
#[cfg(test)]
mod test_ignores {
use super::*;
#[test]
fn empty_matches_nothing() {
let pats = IgnorePatterns::empty();
assert!(!pats.is_matched("nothing"));
assert!(!pats.is_matched("test.mp3"));
}
#[test]
fn ignores_a_glob() {
let (pats, fails) = IgnorePatterns::parse_from_iter(vec![ "*.mp3" ]);
assert!(fails.is_empty());
assert!(!pats.is_matched("nothing"));
assert!(pats.is_matched("test.mp3"));
}
#[test]
fn ignores_an_exact_filename() {
let (pats, fails) = IgnorePatterns::parse_from_iter(vec![ "nothing" ]);
assert!(fails.is_empty());
assert!(pats.is_matched("nothing"));
assert!(!pats.is_matched("test.mp3"));
}
#[test]
fn ignores_both() {
let (pats, fails) = IgnorePatterns::parse_from_iter(vec![ "nothing", "*.mp3" ]);
assert!(fails.is_empty());
assert!(pats.is_matched("nothing"));
assert!(pats.is_matched("test.mp3"));
}
}