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 enum FileFilterFlags {
Reverse,
OnlyDirs,
OnlyFiles,
NoSymlinks,
ShowSymlinks,
ListDirsFirst,
ListDirsLast,
}
#[derive(PartialEq, Eq, Debug, Clone)]
pub struct FileFilter {
pub sort_field: SortField,
pub flags: Vec<FileFilterFlags>,
pub dot_filter: DotFilter,
pub ignore_patterns: IgnorePatterns,
pub git_ignore: GitIgnore,
pub no_symlinks: bool,
pub show_symlinks: bool,
}
impl FileFilter {
#[rustfmt::skip]
pub fn filter_child_files(&self, is_recurse: bool, files: &mut Vec<File<'_>>) {
use FileFilterFlags::{NoSymlinks, OnlyDirs, OnlyFiles, ShowSymlinks};
files.retain(|f| !self.ignore_patterns.is_ignored(&f.name));
files.retain(|f| {
match (
self.flags.contains(&OnlyDirs),
self.flags.contains(&OnlyFiles),
self.flags.contains(&NoSymlinks),
self.flags.contains(&ShowSymlinks),
) {
(true, false, false, false) => f.is_directory(),
(true, false, true, false) => f.is_directory(),
(true, false, false, true) => f.is_directory() || f.points_to_directory(),
(false, true, false, false) => if is_recurse { true } else {f.is_file() },
(false, true, false, true) => if is_recurse { true } else { f.is_file() || f.is_link() && !f.points_to_directory()
},
(false, false, true, false) => !f.is_link(),
_ => true,
}
});
}
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.flags.contains(&FileFilterFlags::Reverse) {
files.reverse();
}
if self.flags.contains(&FileFilterFlags::ListDirsFirst) {
files.sort_by(|a, b| {
b.as_ref()
.points_to_directory()
.cmp(&a.as_ref().points_to_directory())
});
} else if self.flags.contains(&FileFilterFlags::ListDirsLast) {
files.sort_by(|a, b| {
a.as_ref()
.points_to_directory()
.cmp(&b.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};
#[rustfmt::skip]
return 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.length().cmp(&b.length()),
#[cfg(unix)]
Self::FileInode => {
a.metadata().map_or(0, MetadataExt::ino)
.cmp(&b.metadata().map_or(0, MetadataExt::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)
}
#[must_use]
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"));
}
}