use crate::cli::hidden::HiddenKind;
use crate::config::Config;
use crate::error::{MyError, MyResult};
use crate::fs::entry::{Entry, EntryResult, FileEntry};
use crate::fs::file::Signature;
use crate::git::cache::GitCache;
use crate::zip::clone::CloneEntry;
use crate::zip::manager::PasswordManager;
use crate::zip::wrapper::ZipKind;
use std::cell::RefCell;
use std::collections::HashMap;
#[cfg(unix)]
use std::collections::HashSet;
#[cfg(unix)]
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::rc::Rc;
#[cfg(unix)]
use uzers::{gid_t, uid_t, Group, User};
use walkdir::{DirEntry, WalkDir};
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 {
fn walk_entries<F: Fn(EntryResult)>(
&self,
abs_root: &Path,
rel_root: &Path,
git_cache: Option<Rc<GitCache>>,
function: &F,
) -> MyResult<()>;
fn get_entry(&self, path: &Path) -> MyResult<Rc<Box<dyn Entry>>>;
fn read_crc(&self, entry: &dyn Entry) -> u32;
fn read_sig(&self, entry: &dyn Entry) -> Option<Signature>;
#[cfg(windows)]
fn read_version(&self, entry: &dyn Entry) -> Option<String>;
fn read_link(&self, entry: &dyn Entry) -> MyResult<Option<PathBuf>>;
#[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>>;
}
pub struct FileSystem<'a> {
config: &'a Config,
zip_entries: RefCell<HashMap<PathBuf, Rc<Box<dyn Entry>>>>,
zip_manager: RefCell<PasswordManager>,
#[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) -> Self {
let zip_entries = RefCell::new(HashMap::new());
let zip_manager = RefCell::new(PasswordManager::new(config.zip_password()));
let my_uid = uzers::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());
Self { config, zip_entries, zip_manager, my_uid, my_gids, user_names, group_names }
}
#[cfg(unix)]
fn get_gids(uid: uid_t) -> HashSet<gid_t> {
if let Some(groups) = uzers::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) -> Self {
let zip_entries = RefCell::new(HashMap::new());
let zip_manager = RefCell::new(PasswordManager::new(config.zip_password()));
Self { config, zip_entries, zip_manager }
}
fn choose_filter(&self, git_cache: Option<Rc<GitCache>>) -> Box<dyn Fn(&DirEntry) -> bool> {
match self.config.show_hidden() {
HiddenKind::None => {
Box::new(move |entry| Self::exclude_hidden_files(
git_cache.clone(),
entry.file_type().is_dir(),
entry.depth(),
entry.path(),
))
}
HiddenKind::Files => {
Box::new(move |entry| Self::include_hidden_files(
git_cache.clone(),
entry.file_type().is_dir(),
entry.depth(),
entry.path(),
))
}
HiddenKind::Recurse => {
Box::new(move |entry| Self::recurse_hidden_files(
git_cache.clone(),
entry.file_type().is_dir(),
entry.path(),
))
}
}
}
fn recurse_hidden_files(
git_cache: Option<Rc<GitCache>>,
is_dir: bool,
path: &Path,
) -> bool {
if is_dir && Self::is_ignored_dir(git_cache, path) {
return false;
}
true
}
fn include_hidden_files(
git_cache: Option<Rc<GitCache>>,
is_dir: bool,
depth: usize,
path: &Path,
) -> bool {
if depth > 0 {
if is_dir && Self::is_ignored_dir(git_cache, path) {
return false;
}
}
if depth > 1 {
if let Some(parent) = path.parent() {
if let Some(name) = parent.file_name() {
if Self::is_hidden_name(name.to_str()) {
return false;
}
}
}
}
true
}
fn exclude_hidden_files(
git_cache: Option<Rc<GitCache>>,
is_dir: bool,
depth: usize,
path: &Path,
) -> bool {
if depth > 0 {
if is_dir && Self::is_ignored_dir(git_cache, path) {
return false;
}
let name = path.file_name().unwrap_or_else(|| path.as_os_str());
if Self::is_hidden_name(name.to_str()) {
return false;
}
}
true
}
fn is_ignored_dir(git_cache: Option<Rc<GitCache>>, path: &Path) -> bool {
if let Some(git_cache) = git_cache {
git_cache.test_ignored(path)
} else {
false
}
}
pub fn is_hidden_name(name: Option<&str>) -> bool {
if let Some(name) = name {
if name.starts_with(".") {
return true;
}
if name.starts_with("__") && name.ends_with("__") {
return true;
}
}
false
}
fn walk_entry<F: Fn(EntryResult)>(&self, entry: DirEntry, function: &F) -> MyResult<()> {
let zip_expand = self.config.zip_expand() && entry.file_type().is_file();
if let Some(zip_kind) = ZipKind::from_path(entry.path(), zip_expand) {
let mut zip_manager = self.zip_manager.borrow_mut();
zip_kind.walk_entries(self.config, &entry, &mut zip_manager, &|result| {
match result {
Ok(entry) => {
self.clone_entry(entry);
function(Ok(entry));
}
Err(error) => {
function(Err(error));
}
}
})?;
let entry = FileEntry::from_entry(entry, true);
self.clone_entry(entry.as_ref());
function(Ok(entry.as_ref()));
} else {
let entry = FileEntry::from_entry(entry, false);
function(Ok(entry.as_ref()));
}
Ok(())
}
fn clone_entry(&self, entry: &dyn Entry) {
let path = PathBuf::from(entry.file_path());
let entry = CloneEntry::from_entry(entry);
self.zip_entries.borrow_mut().insert(path, entry);
}
#[cfg(unix)]
fn get_uid_name(uid: &uid_t) -> Option<Rc<String>> {
uzers::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>> {
uzers::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 for FileSystem<'a> {
fn walk_entries<F: Fn(EntryResult)>(
&self,
abs_root: &Path,
_rel_root: &Path,
git_cache: Option<Rc<GitCache>>,
function: &F,
) -> MyResult<()> {
let mut walker = WalkDir::new(abs_root);
if let Some(depth) = self.config.max_depth() {
walker = walker.max_depth(depth);
}
let filter = self.choose_filter(git_cache);
for entry in walker.into_iter().filter_entry(filter) {
match entry {
Ok(entry) => self.walk_entry(entry, function)?,
Err(error) => function(Err(MyError::from(error))),
}
}
Ok(())
}
fn get_entry(&self, path: &Path) -> MyResult<Rc<Box<dyn Entry>>> {
if let Some(entry) = self.zip_entries.borrow().get(path) {
Ok(Rc::clone(entry))
} else {
let entry = FileEntry::from_path(path)?;
Ok(Rc::new(entry))
}
}
fn read_crc(&self, entry: &dyn Entry) -> u32 {
entry.read_crc()
}
fn read_sig(&self, entry: &dyn Entry) -> Option<Signature> {
entry.read_sig()
}
#[cfg(windows)]
fn read_version(&self, entry: &dyn Entry) -> Option<String> {
entry.read_version()
}
fn read_link(&self, entry: &dyn Entry) -> MyResult<Option<PathBuf>> {
entry.read_link()
}
#[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(Self::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(Self::get_gid_name)
.as_ref()
.map(Rc::clone)
}
}
#[cfg(test)]
pub mod tests {
use crate::config::Config;
use crate::error::{MyError, MyResult};
use crate::fs::entry::{Entry, EntryResult};
use crate::fs::file::Signature;
use crate::fs::metadata::Metadata;
#[cfg(unix)]
use crate::fs::system::EXEC_MASK;
use crate::fs::system::{FileEntry, FileSystem, System};
use crate::git::cache::GitCache;
use pretty_assertions::assert_eq;
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::rc::Rc;
#[cfg(unix)]
use uzers::{gid_t, uid_t};
#[test]
fn test_shows_hidden_directories_and_shows_contents() {
assert_eq!(true, FileSystem::recurse_hidden_files(None, false, &PathBuf::from("/tmp/test")));
assert_eq!(true, FileSystem::recurse_hidden_files(None, false, &PathBuf::from("/tmp/.test")));
assert_eq!(true, FileSystem::recurse_hidden_files(None, false, &PathBuf::from("/tmp/test/visible")));
assert_eq!(true, FileSystem::recurse_hidden_files(None, false, &PathBuf::from("/tmp/.test/visible")));
assert_eq!(true, FileSystem::recurse_hidden_files(None, false, &PathBuf::from("/tmp/test/visible/file")));
assert_eq!(true, FileSystem::recurse_hidden_files(None, false, &PathBuf::from("/tmp/.test/visible/file")));
assert_eq!(true, FileSystem::recurse_hidden_files(None, false, &PathBuf::from("/tmp/test/.hidden")));
assert_eq!(true, FileSystem::recurse_hidden_files(None, false, &PathBuf::from("/tmp/.test/.hidden")));
assert_eq!(true, FileSystem::recurse_hidden_files(None, false, &PathBuf::from("/tmp/test/.hidden/file")));
assert_eq!(true, FileSystem::recurse_hidden_files(None, false, &PathBuf::from("/tmp/.test/.hidden/file")));
}
#[test]
fn test_shows_hidden_directories_and_hides_contents() {
assert_eq!(true, FileSystem::include_hidden_files(None, false, 0, &PathBuf::from("/tmp/test")));
assert_eq!(true, FileSystem::include_hidden_files(None, false, 0, &PathBuf::from("/tmp/.test")));
assert_eq!(true, FileSystem::include_hidden_files(None, false, 1, &PathBuf::from("/tmp/test/visible")));
assert_eq!(true, FileSystem::include_hidden_files(None, false, 1, &PathBuf::from("/tmp/.test/visible")));
assert_eq!(true, FileSystem::include_hidden_files(None, false, 2, &PathBuf::from("/tmp/test/visible/file")));
assert_eq!(true, FileSystem::include_hidden_files(None, false, 2, &PathBuf::from("/tmp/.test/visible/file")));
assert_eq!(true, FileSystem::include_hidden_files(None, false, 1, &PathBuf::from("/tmp/test/.hidden")));
assert_eq!(true, FileSystem::include_hidden_files(None, false, 1, &PathBuf::from("/tmp/.test/.hidden")));
assert_eq!(false, FileSystem::include_hidden_files(None, false, 2, &PathBuf::from("/tmp/test/.hidden/file")));
assert_eq!(false, FileSystem::include_hidden_files(None, false, 2, &PathBuf::from("/tmp/.test/.hidden/file")));
}
#[test]
fn test_hides_hidden_directories_and_hides_contents() {
assert_eq!(true, FileSystem::exclude_hidden_files(None, false, 0, &PathBuf::from("/tmp/test")));
assert_eq!(true, FileSystem::exclude_hidden_files(None, false, 0, &PathBuf::from("/tmp/.test")));
assert_eq!(true, FileSystem::exclude_hidden_files(None, false, 1, &PathBuf::from("/tmp/test/visible")));
assert_eq!(true, FileSystem::exclude_hidden_files(None, false, 1, &PathBuf::from("/tmp/.test/visible")));
assert_eq!(true, FileSystem::exclude_hidden_files(None, false, 2, &PathBuf::from("/tmp/test/visible/file")));
assert_eq!(true, FileSystem::exclude_hidden_files(None, false, 2, &PathBuf::from("/tmp/.test/visible/file")));
assert_eq!(false, FileSystem::exclude_hidden_files(None, false, 1, &PathBuf::from("/tmp/test/.hidden")));
assert_eq!(false, FileSystem::exclude_hidden_files(None, false, 1, &PathBuf::from("/tmp/.test/.hidden")));
assert_eq!(true, FileSystem::exclude_hidden_files(None, false, 2, &PathBuf::from("/tmp/test/.hidden/file")));
assert_eq!(true, FileSystem::exclude_hidden_files(None, false, 2, &PathBuf::from("/tmp/.test/.hidden/file")));
}
#[test]
fn test_detects_hidden_names() {
assert_eq!(false, FileSystem::is_hidden_name(None));
assert_eq!(false, FileSystem::is_hidden_name(Some("")));
assert_eq!(false, FileSystem::is_hidden_name(Some("visible")));
assert_eq!(false, FileSystem::is_hidden_name(Some("visible__")));
assert_eq!(false, FileSystem::is_hidden_name(Some("_visible_")));
assert_eq!(false, FileSystem::is_hidden_name(Some("__visible")));
assert_eq!(true, FileSystem::is_hidden_name(Some(".hidden")));
assert_eq!(true, FileSystem::is_hidden_name(Some("__hidden__")));
}
pub struct MockSystem<'a> {
config: &'a Config,
current: PathBuf,
entries: BTreeMap<PathBuf, FileEntry>,
links: BTreeMap<PathBuf, PathBuf>,
#[cfg(unix)]
user_names: BTreeMap<uid_t, String>,
#[cfg(unix)]
group_names: BTreeMap<gid_t, String>,
}
impl<'a> MockSystem<'a> {
pub fn new(
config: &'a Config,
current: PathBuf,
#[cfg(unix)]
user_names: BTreeMap<uid_t, String>,
#[cfg(unix)]
group_names: BTreeMap<uid_t, String>,
) -> Self {
let entries = BTreeMap::new();
let links = BTreeMap::new();
Self {
config,
current,
entries,
links,
#[cfg(unix)]
user_names,
#[cfg(unix)]
group_names,
}
}
pub fn insert_entry(
&mut self,
file_depth: usize,
file_type: char,
file_mode: u32,
owner_uid: u32, owner_gid: u32, file_size: u64,
file_year: i32,
file_month: u32,
file_day: u32,
file_path: &str,
link_path: Option<&str>,
) {
let file_path = self.current.join(file_path);
let metadata = Metadata::from_fields(
file_type,
file_mode,
owner_uid,
owner_gid,
file_size,
file_year,
file_month,
file_day,
);
let entry = FileEntry::from_fields(
file_path.clone(),
file_depth,
file_type,
metadata.clone(),
);
self.entries.insert(file_path.clone(), entry);
if let Some(link_path) = link_path {
let link_path = PathBuf::from(link_path);
self.links.insert(file_path, link_path);
}
}
fn filter_depth(&self, entry: &FileEntry) -> bool {
match self.config.max_depth() {
Some(depth) => entry.file_depth() <= depth,
None => true,
}
}
}
impl<'a> System for MockSystem<'a> {
fn walk_entries<F: Fn(EntryResult)>(
&self,
abs_root: &Path,
rel_root: &Path,
_git_cache: Option<Rc<GitCache>>,
function: &F,
) -> MyResult<()> {
let rel_depth = rel_root.components().count();
for (_, entry) in self.entries.iter() {
if let Some(entry) = entry.subtract_depth(rel_depth) {
if self.filter_depth(&entry) && entry.file_path().starts_with(abs_root) {
function(Ok(&entry));
}
}
}
Ok(())
}
fn get_entry(&self, path: &Path) -> MyResult<Rc<Box<dyn Entry>>> {
let entry = self.entries
.get(path)
.map(|entry| entry.clone())
.ok_or(MyError::Text(format!("Entry not found: {}", path.display())))?;
Ok(Rc::new(Box::new(entry)))
}
fn read_crc(&self, _entry: &dyn Entry) -> u32 {
0
}
fn read_sig(&self, _entry: &dyn Entry) -> Option<Signature> {
None
}
#[cfg(windows)]
fn read_version(&self, _entry: &dyn Entry) -> Option<String> {
None
}
fn read_link(&self, entry: &dyn Entry) -> MyResult<Option<PathBuf>> {
let path = entry.file_path();
match self.links.get(path) {
Some(link) => Ok(Some(link.clone())),
None => Err(MyError::Text(format!("Link not found: {}", path.display()))),
}
}
#[cfg(unix)]
fn get_mask(&self, _uid: uid_t, _gid: gid_t) -> u32 {
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)
}
}
}