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 list_dirs_first: bool,
pub sort_field: SortField,
pub reverse: bool,
pub only_dirs: bool,
pub dot_filter: DotFilter,
pub ignore_patterns: IgnorePatterns,
pub git_ignore: GitIgnore,
}
impl FileFilter {
pub fn filter_child_files(&self, files: &mut Vec<File<'_>>) {
files.retain(|f| ! self.ignore_patterns.is_ignored(&f.name));
if self.only_dirs {
files.retain(File::is_directory);
}
}
pub fn filter_argument_files(&self, files: &mut Vec<File<'_>>) {
files.retain(|f| {
! self.ignore_patterns.is_ignored(&f.name)
});
}
pub fn sort_files<'a, F>(&self, files: &mut [F])
where F: AsRef<File<'a>>
{
files.sort_by(|a, b| {
self.sort_field.compare_files(a.as_ref(), b.as_ref())
});
if self.reverse {
files.reverse();
}
if self.list_dirs_first {
files.sort_by(|a, b| {
b.as_ref().points_to_directory()
.cmp(&a.as_ref().points_to_directory())
});
}
}
}
#[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() }
}
fn is_ignored(&self, file: &str) -> bool {
self.patterns.iter().any(|p| p.matches(file))
}
}
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
pub enum GitIgnore {
CheckAndIgnore,
Off,
}
#[cfg(test)]
mod test_ignores {
use super::*;
#[test]
fn empty_matches_nothing() {
let pats = IgnorePatterns::empty();
assert!(!pats.is_ignored("nothing"));
assert!(!pats.is_ignored("test.mp3"));
}
#[test]
fn ignores_a_glob() {
let (pats, fails) = IgnorePatterns::parse_from_iter(vec![ "*.mp3" ]);
assert!(fails.is_empty());
assert!(!pats.is_ignored("nothing"));
assert!(pats.is_ignored("test.mp3"));
}
#[test]
fn ignores_an_exact_filename() {
let (pats, fails) = IgnorePatterns::parse_from_iter(vec![ "nothing" ]);
assert!(fails.is_empty());
assert!(pats.is_ignored("nothing"));
assert!(!pats.is_ignored("test.mp3"));
}
#[test]
fn ignores_both() {
let (pats, fails) = IgnorePatterns::parse_from_iter(vec![ "nothing", "*.mp3" ]);
assert!(fails.is_empty());
assert!(pats.is_ignored("nothing"));
assert!(pats.is_ignored("test.mp3"));
}
}