use std::ffi::OsStr;
use std::fs::FileType;
use std::path::{Path, PathBuf};
use walkdir::{DirEntry, FilterEntry, IntoIter, WalkDir};
use crate::config::Config;
use crate::error::{MyError, MyResult};
use crate::metadata::Metadata;
#[cfg(unix)]
use {
std::cell::RefCell,
std::collections::{HashMap, HashSet},
std::rc::Rc,
uzers::{get_effective_uid, get_group_by_gid, get_user_by_uid, gid_t, uid_t, Group, User},
};
pub trait Flags {
fn is_file(&self) -> bool;
fn is_dir(&self) -> bool;
fn is_symlink(&self) -> bool;
}
pub trait Entry<F: Flags + Copy> {
fn path(&self) -> &Path;
fn file_name(&self) -> &OsStr;
fn file_type(&self) -> F;
fn depth(&self) -> usize;
fn metadata(&self) -> MyResult<Metadata<F>>;
fn read_link(&self) -> MyResult<Option<PathBuf>>;
}
pub const OWNER_MASK: u32 = 0o100;
pub const GROUP_MASK: u32 = 0o010;
pub const OTHER_MASK: u32 = 0o001;
pub const EXEC_MASK: u32 = 0o111;
pub trait System<F, E, I> where
F: Flags + Copy,
E: Entry<F>,
I: Iterator<Item = MyResult<E>>,
{
fn entries(&self, abs_root: &Path, rel_root: &Path) -> I;
fn metadata(&self, path: &Path) -> MyResult<Metadata<F>>;
#[cfg(unix)]
fn get_mask(&self, uid: uid_t, gid: gid_t) -> u32;
#[cfg(unix)]
fn find_user(&self, uid: uid_t) -> Option<Rc<String>>;
#[cfg(unix)]
fn find_group(&self, gid: gid_t) -> Option<Rc<String>>;
}
#[derive(Copy, Clone)]
pub struct FileFlags {
file_type: FileType,
}
impl FileFlags {
pub fn new(file_type: FileType) -> FileFlags {
Self { file_type }
}
}
impl Flags for FileFlags {
fn is_file(&self) -> bool {
self.file_type.is_file()
}
fn is_dir(&self) -> bool {
self.file_type.is_dir()
}
fn is_symlink(&self) -> bool {
self.file_type.is_symlink()
}
}
pub struct FileEntry {
entry: DirEntry,
}
impl FileEntry {
fn new(entry: DirEntry) -> FileEntry {
Self { entry }
}
}
impl Entry<FileFlags> for FileEntry {
fn path(&self) -> &Path {
self.entry.path()
}
fn file_name(&self) -> &OsStr {
self.entry.file_name()
}
fn file_type(&self) -> FileFlags {
FileFlags::new(self.entry.file_type())
}
fn depth(&self) -> usize {
self.entry.depth()
}
fn metadata(&self) -> MyResult<Metadata<FileFlags>> {
Metadata::from_entry(&self.entry)
}
fn read_link(&self) -> MyResult<Option<PathBuf>> {
return if self.entry.path_is_symlink() {
let link = self.entry.path().read_link()?;
Ok(Some(link))
} else {
Ok(None)
}
}
}
pub struct FileIterator {
iterator: FilterEntry<IntoIter, fn(&DirEntry) -> bool>,
}
impl FileIterator {
fn new(iterator: FilterEntry<IntoIter, fn(&DirEntry) -> bool>) -> FileIterator {
Self { iterator }
}
}
impl Iterator for FileIterator {
type Item = MyResult<FileEntry>;
fn next(&mut self) -> Option<Self::Item> {
match self.iterator.next() {
Some(Ok(entry)) => Some(Ok(FileEntry::new(entry))),
Some(Err(error)) => Some(Err(MyError::from(error))),
None => None,
}
}
}
pub struct FileSystem<'a> {
config: &'a Config,
#[cfg(unix)]
my_uid: uid_t,
#[cfg(unix)]
my_gids: HashSet<gid_t>,
#[cfg(unix)]
user_names: RefCell<HashMap<uid_t, Option<Rc<String>>>>,
#[cfg(unix)]
group_names: RefCell<HashMap<gid_t, Option<Rc<String>>>>,
}
impl<'a> FileSystem<'a> {
#[cfg(unix)]
pub fn new(config: &'a Config) -> FileSystem {
let my_uid = get_effective_uid();
let my_gids = Self::get_gids(my_uid);
let user_names = RefCell::new(HashMap::new());
let group_names = RefCell::new(HashMap::new());
return Self { config, my_uid, my_gids, user_names, group_names };
}
#[cfg(unix)]
fn get_gids(uid: uid_t) -> HashSet<gid_t> {
if let Some(groups) = get_user_by_uid(uid).as_ref().and_then(User::groups) {
groups.iter().map(Group::gid).collect()
} else {
HashSet::new()
}
}
#[cfg(not(unix))]
pub fn new(config: &'a Config) -> FileSystem {
Self { config }
}
fn choose_filter(&self) -> fn(&DirEntry) -> bool {
if self.config.all_recurse {
|_| true
} else if self.config.all_files {
Self::filter_hidden_parent
} else {
Self::filter_hidden_entry
}
}
fn filter_hidden_parent(entry: &DirEntry) -> bool {
if entry.depth() > 1 {
if let Some(parent) = entry.path().parent() {
if let Some(name) = parent.file_name() {
return Self::filter_hidden_name(name.to_str());
}
}
}
return true;
}
fn filter_hidden_entry(entry: &DirEntry) -> bool {
Self::filter_hidden_name(entry.file_name().to_str())
}
fn filter_hidden_name(name: Option<&str>) -> bool {
if let Some(name) = name {
if name.starts_with(".") {
return false;
}
if name.starts_with("__") && name.ends_with("__") {
return false;
}
}
return true;
}
#[cfg(unix)]
fn get_uid_name(uid: &uid_t) -> Option<Rc<String>> {
get_user_by_uid(*uid)
.as_ref()
.map(User::name)
.and_then(OsStr::to_str)
.map(str::to_string)
.map(Rc::new)
}
#[cfg(unix)]
fn get_gid_name(gid: &gid_t) -> Option<Rc<String>> {
get_group_by_gid(*gid)
.as_ref()
.map(Group::name)
.and_then(OsStr::to_str)
.map(str::to_string)
.map(Rc::new)
}
}
impl<'a> System<FileFlags, FileEntry, FileIterator> for FileSystem<'a> {
fn entries(&self, abs_root: &Path, _rel_root: &Path) -> FileIterator {
let mut walker = WalkDir::new(abs_root).min_depth(1);
if let Some(depth) = self.config.max_depth {
walker = walker.max_depth(depth);
}
let filter = self.choose_filter();
let iterator = walker.into_iter().filter_entry(filter);
return FileIterator::new(iterator);
}
fn metadata(&self, path: &Path) -> MyResult<Metadata<FileFlags>> {
Metadata::from_path(path)
}
#[cfg(unix)]
fn get_mask(&self, uid: uid_t, gid: gid_t) -> u32 {
if uid == self.my_uid {
OWNER_MASK
} else if self.my_gids.contains(&gid) {
GROUP_MASK
} else {
OTHER_MASK
}
}
#[cfg(unix)]
fn find_user(&self, uid: uid_t) -> Option<Rc<String>> {
self.user_names.borrow_mut()
.entry(uid)
.or_insert_with_key(FileSystem::get_uid_name)
.as_ref()
.map(Rc::clone)
}
#[cfg(unix)]
fn find_group(&self, gid: gid_t) -> Option<Rc<String>> {
self.group_names.borrow_mut()
.entry(gid)
.or_insert_with_key(FileSystem::get_gid_name)
.as_ref()
.map(Rc::clone)
}
}
#[cfg(test)]
pub mod tests {
use std::collections::HashMap;
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::time::SystemTime;
use std::vec::IntoIter;
use chrono::{DateTime, TimeZone, Utc};
use pretty_assertions::assert_eq;
use crate::config::Config;
use crate::error::{MyError, MyResult};
use crate::metadata::Metadata;
use crate::system::{Entry, FileSystem, Flags, System};
#[cfg(unix)]
use {
crate::system::EXEC_MASK,
std::rc::Rc,
uzers::{gid_t, uid_t},
};
#[test]
fn test_hides_hidden_files() {
assert_eq!(true, FileSystem::filter_hidden_name(None));
assert_eq!(true, FileSystem::filter_hidden_name(Some("")));
assert_eq!(true, FileSystem::filter_hidden_name(Some("visible")));
assert_eq!(true, FileSystem::filter_hidden_name(Some("visible__")));
assert_eq!(true, FileSystem::filter_hidden_name(Some("_visible_")));
assert_eq!(true, FileSystem::filter_hidden_name(Some("__visible")));
assert_eq!(false, FileSystem::filter_hidden_name(Some(".hidden")));
assert_eq!(false, FileSystem::filter_hidden_name(Some("__hidden__")));
}
#[derive(Copy, Clone)]
pub struct MockFlags {
file_type: char,
}
impl MockFlags {
fn new(file_type: char) -> MockFlags {
Self { file_type }
}
}
impl Flags for MockFlags {
fn is_file(&self) -> bool {
self.file_type == 'f'
}
fn is_dir(&self) -> bool {
self.file_type == 'd'
}
fn is_symlink(&self) -> bool {
self.file_type == 'l'
}
}
#[derive(Clone)]
pub struct MockEntry {
path: PathBuf,
file_type: MockFlags,
depth: usize,
metadata: Metadata<MockFlags>,
link: Option<Box<MockEntry>>,
}
#[allow(unused_variables)]
impl MockEntry {
pub fn new(
depth: usize,
file_type: char,
file_mode: u32,
owner_uid: u32, owner_gid: u32, file_size: u64,
year: i32,
month: u32,
day: u32,
path: &str,
link: Option<MockEntry>,
) -> MockEntry {
let path = PathBuf::from(path);
let file_type = MockFlags::new(file_type);
let file_time = create_time(year, month, day);
let file_time = SystemTime::from(file_time);
let metadata = Metadata {
file_type,
file_mode,
#[cfg(unix)]
owner_uid,
#[cfg(unix)]
owner_gid,
file_size,
file_time,
};
let link = link.map(Box::new);
return Self { path, file_type, depth, metadata, link };
}
fn replace_root(mut self, abs_root: &Path, rel_root: &Path) -> Option<MockEntry> {
match self.path.strip_prefix(rel_root) {
Ok(path) => {
self.path = abs_root.join(path);
Some(self)
}
Err(_) => None
}
}
}
impl Entry<MockFlags> for MockEntry {
fn path(&self) -> &Path {
&self.path
}
fn file_name(&self) -> &OsStr {
self.path.file_name().unwrap_or_default()
}
fn file_type(&self) -> MockFlags {
self.file_type
}
fn depth(&self) -> usize {
self.depth
}
fn metadata(&self) -> MyResult<Metadata<MockFlags>> {
Ok(self.metadata.clone())
}
fn read_link(&self) -> MyResult<Option<PathBuf>> {
let path = self.link.as_ref().map(|link| link.path.clone());
return Ok(path);
}
}
pub struct MockIterator {
entries: IntoIter<MockEntry>,
}
impl MockIterator {
fn new(entries: Vec<MockEntry>) -> MockIterator {
Self { entries: entries.into_iter() }
}
}
impl Iterator for MockIterator {
type Item = MyResult<MockEntry>;
fn next(&mut self) -> Option<Self::Item> {
self.entries.next().map(Ok)
}
}
pub struct MockSystem<'a> {
config: &'a Config,
entries: Vec<MockEntry>,
metadata: HashMap<PathBuf, Metadata<MockFlags>>,
#[cfg(unix)]
user_names: HashMap<uid_t, String>,
#[cfg(unix)]
group_names: HashMap<gid_t, String>,
}
impl<'a> MockSystem<'a> {
pub fn new(
config: &'a Config,
current: &Path,
entries: Vec<MockEntry>,
#[cfg(unix)]
user_names: HashMap<uid_t, String>,
#[cfg(unix)]
group_names: HashMap<uid_t, String>,
) -> MockSystem<'a> {
let mut metadata = HashMap::new();
for entry in entries.iter() {
Self::add_metadata(&mut metadata, current, entry);
if let Some(link) = &entry.link {
if !link.path.starts_with("/") {
let parent = entry.path.parent().unwrap();
let parent = current.join(parent);
Self::add_metadata(&mut metadata, &parent, link);
}
}
}
#[cfg(unix)]
return Self { config, entries, metadata, user_names, group_names };
#[cfg(not(unix))]
return Self { config, entries, metadata };
}
fn add_metadata(
metadata: &mut HashMap<PathBuf, Metadata<MockFlags>>,
parent: &Path,
entry: &MockEntry,
) {
let path = parent.join(&entry.path);
metadata.insert(path, entry.metadata.clone());
}
fn subtract_depth(&self, entry: &MockEntry, depth: usize) -> Option<MockEntry> {
if entry.depth >= depth {
let mut entry = entry.clone();
entry.depth -= depth;
return Some(entry);
}
return None;
}
fn filter_depth(&self, entry: &MockEntry) -> bool {
match self.config.max_depth {
Some(depth) => entry.depth <= depth,
None => true,
}
}
}
impl<'a> System<MockFlags, MockEntry, MockIterator> for MockSystem<'a> {
fn entries(&self, abs_root: &Path, rel_root: &Path) -> MockIterator {
let rel_depth = rel_root.components().count();
let entries = self.entries.clone()
.into_iter()
.flat_map(|entry| self.subtract_depth(&entry, rel_depth))
.filter(|entry| self.filter_depth(entry))
.flat_map(|entry| entry.replace_root(abs_root, rel_root))
.collect();
return MockIterator::new(entries);
}
fn metadata(&self, path: &Path) -> MyResult<Metadata<MockFlags>> {
match self.metadata.get(path) {
Some(metadata) => {
let metadata = metadata.clone();
return Ok(metadata);
}
None => {
let error = format!("File not found: {}", path.display());
return Err(MyError::Text(error));
}
}
}
#[cfg(unix)]
fn get_mask(&self, _uid: uid_t, _gid: gid_t) -> u32 {
return EXEC_MASK;
}
#[cfg(unix)]
fn find_user(&self, uid: uid_t) -> Option<Rc<String>> {
self.user_names.get(&uid).map(String::clone).map(Rc::new)
}
#[cfg(unix)]
fn find_group(&self, gid: gid_t) -> Option<Rc<String>> {
self.group_names.get(&gid).map(String::clone).map(Rc::new)
}
}
fn create_time(year: i32, month: u32, day: u32) -> DateTime<Utc> {
Utc.with_ymd_and_hms(year, month, day, 0, 0, 0).unwrap()
}
}