Skip to main content

ex_cli/fs/
system.rs

1use crate::cli::hidden::HiddenKind;
2use crate::config::Config;
3use crate::error::{MyError, MyResult};
4use crate::fs::entry::{Entry, EntryResult, FileEntry};
5use crate::fs::file::Signature;
6use crate::git::cache::GitCache;
7use crate::zip::clone::CloneEntry;
8use crate::zip::manager::PasswordManager;
9use crate::zip::wrapper::ZipKind;
10use std::cell::RefCell;
11use std::collections::HashMap;
12#[cfg(unix)]
13use std::collections::HashSet;
14#[cfg(unix)]
15use std::ffi::OsStr;
16use std::path::{Path, PathBuf};
17use std::rc::Rc;
18#[cfg(unix)]
19use uzers::{gid_t, uid_t, Group, User};
20use walkdir::{DirEntry, WalkDir};
21
22pub const OWNER_MASK: u32 = 0o100;
23pub const GROUP_MASK: u32 = 0o010;
24pub const OTHER_MASK: u32 = 0o001;
25pub const EXEC_MASK: u32 = 0o111;
26
27pub trait System {
28    fn walk_entries<F: Fn(EntryResult)>(
29        &self,
30        abs_root: &Path,
31        rel_root: &Path,
32        git_cache: Option<Rc<GitCache>>,
33        function: &F,
34    ) -> MyResult<()>;
35
36    fn get_entry(&self, path: &Path) -> MyResult<Rc<Box<dyn Entry>>>;
37
38    fn read_sig(&self, entry: &dyn Entry) -> Option<Signature>;
39
40    #[cfg(windows)]
41    fn read_version(&self, entry: &dyn Entry) -> Option<String>;
42
43    fn read_link(&self, entry: &dyn Entry) -> MyResult<Option<PathBuf>>;
44
45    #[cfg(unix)]
46    fn get_mask(&self, uid: uid_t, gid: gid_t) -> u32;
47
48    #[cfg(unix)]
49    fn find_user(&self, uid: uid_t) -> Option<Rc<String>>;
50
51    #[cfg(unix)]
52    fn find_group(&self, gid: gid_t) -> Option<Rc<String>>;
53}
54
55pub struct FileSystem<'a> {
56    config: &'a Config,
57    zip_entries: RefCell<HashMap<PathBuf, Rc<Box<dyn Entry>>>>,
58    zip_manager: RefCell<PasswordManager>,
59    #[cfg(unix)]
60    my_uid: uid_t,
61    #[cfg(unix)]
62    my_gids: HashSet<gid_t>,
63    #[cfg(unix)]
64    user_names: RefCell<HashMap<uid_t, Option<Rc<String>>>>,
65    #[cfg(unix)]
66    group_names: RefCell<HashMap<gid_t, Option<Rc<String>>>>,
67}
68
69impl<'a> FileSystem<'a> {
70    #[cfg(unix)]
71    pub fn new(config: &'a Config) -> Self {
72        let zip_entries = RefCell::new(HashMap::new());
73        let zip_manager = RefCell::new(PasswordManager::new(config.zip_password()));
74        let my_uid = uzers::get_effective_uid();
75        let my_gids = Self::get_gids(my_uid);
76        let user_names = RefCell::new(HashMap::new());
77        let group_names = RefCell::new(HashMap::new());
78        Self { config, zip_entries, zip_manager, my_uid, my_gids, user_names, group_names }
79    }
80
81    #[cfg(unix)]
82    fn get_gids(uid: uid_t) -> HashSet<gid_t> {
83        if let Some(groups) = uzers::get_user_by_uid(uid).as_ref().and_then(User::groups) {
84            groups.iter().map(Group::gid).collect()
85        } else {
86            HashSet::new()
87        }
88    }
89
90    #[cfg(not(unix))]
91    pub fn new(config: &'a Config) -> Self {
92        let zip_entries = RefCell::new(HashMap::new());
93        let zip_manager = RefCell::new(PasswordManager::new(config.zip_password()));
94        Self { config, zip_entries, zip_manager }
95    }
96
97    fn choose_filter(&self, git_cache: Option<Rc<GitCache>>) -> Box<dyn Fn(&DirEntry) -> bool> {
98        match self.config.show_hidden() {
99            HiddenKind::None => {
100                Box::new(move |entry| Self::exclude_hidden_files(
101                    git_cache.clone(),
102                    entry.file_type().is_dir(),
103                    entry.depth(),
104                    entry.path(),
105                ))
106            }
107            HiddenKind::Files => {
108                Box::new(move |entry| Self::include_hidden_files(
109                    git_cache.clone(),
110                    entry.file_type().is_dir(),
111                    entry.depth(),
112                    entry.path(),
113                ))
114            }
115            HiddenKind::Recurse => {
116                Box::new(move |entry| Self::recurse_hidden_files(
117                    git_cache.clone(),
118                    entry.file_type().is_dir(),
119                    entry.path(),
120                ))
121            }
122        }
123    }
124
125    fn recurse_hidden_files(
126        git_cache: Option<Rc<GitCache>>,
127        is_dir: bool,
128        path: &Path,
129    ) -> bool {
130        if is_dir && Self::is_ignored_dir(git_cache, path) {
131            return false;
132        }
133        true
134    }
135
136    fn include_hidden_files(
137        git_cache: Option<Rc<GitCache>>,
138        is_dir: bool,
139        depth: usize,
140        path: &Path,
141    ) -> bool {
142        if depth > 1 {
143            if is_dir && Self::is_ignored_dir(git_cache, path) {
144                return false;
145            }
146            if let Some(parent) = path.parent() {
147                if let Some(name) = parent.file_name() {
148                    if Self::is_hidden_name(name.to_str()) {
149                        return false;
150                    }
151                }
152            }
153        }
154        true
155    }
156
157    fn exclude_hidden_files(
158        git_cache: Option<Rc<GitCache>>,
159        is_dir: bool,
160        depth: usize,
161        path: &Path,
162    ) -> bool {
163        if depth > 0 {
164            if is_dir && Self::is_ignored_dir(git_cache, path) {
165                return false;
166            }
167            let name = path.file_name().unwrap_or_else(|| path.as_os_str());
168            if Self::is_hidden_name(name.to_str()) {
169                return false;
170            }
171        }
172        true
173    }
174
175    fn is_ignored_dir(git_cache: Option<Rc<GitCache>>, path: &Path) -> bool {
176        if let Some(git_cache) = git_cache {
177            git_cache.test_ignored(path)
178        } else {
179            false
180        }
181    }
182
183    pub fn is_hidden_name(name: Option<&str>) -> bool {
184        if let Some(name) = name {
185            if name.starts_with(".") {
186                return true;
187            }
188            if name.starts_with("__") && name.ends_with("__") {
189                return true;
190            }
191        }
192        false
193    }
194
195    fn walk_entry<F: Fn(EntryResult)>(&self, entry: DirEntry, function: &F) -> MyResult<()> {
196        let zip_expand = self.config.zip_expand() && entry.file_type().is_file();
197        if let Some(zip_kind) = ZipKind::from_path(entry.path(), zip_expand) {
198            let mut zip_manager = self.zip_manager.borrow_mut();
199            zip_kind.walk_entries(self.config, &entry, &mut zip_manager, &|result| {
200                match result {
201                    Ok(entry) => {
202                        self.clone_entry(entry);
203                        function(Ok(entry));
204                    }
205                    Err(error) => {
206                        function(Err(error));
207                    }
208                }
209            })?;
210            let entry = FileEntry::from_entry(entry, true);
211            self.clone_entry(entry.as_ref());
212            function(Ok(entry.as_ref()));
213        } else {
214            let entry = FileEntry::from_entry(entry, false);
215            function(Ok(entry.as_ref()));
216        }
217        Ok(())
218    }
219
220    fn clone_entry(&self, entry: &dyn Entry) {
221        let path = PathBuf::from(entry.file_path());
222        let entry = CloneEntry::from_entry(entry);
223        self.zip_entries.borrow_mut().insert(path, entry);
224    }
225
226    #[cfg(unix)]
227    fn get_uid_name(uid: &uid_t) -> Option<Rc<String>> {
228        uzers::get_user_by_uid(*uid)
229            .as_ref()
230            .map(User::name)
231            .and_then(OsStr::to_str)
232            .map(str::to_string)
233            .map(Rc::new)
234    }
235
236    #[cfg(unix)]
237    fn get_gid_name(gid: &gid_t) -> Option<Rc<String>> {
238        uzers::get_group_by_gid(*gid)
239            .as_ref()
240            .map(Group::name)
241            .and_then(OsStr::to_str)
242            .map(str::to_string)
243            .map(Rc::new)
244    }
245}
246
247impl<'a> System for FileSystem<'a> {
248    fn walk_entries<F: Fn(EntryResult)>(
249        &self,
250        abs_root: &Path,
251        _rel_root: &Path,
252        git_cache: Option<Rc<GitCache>>,
253        function: &F,
254    ) -> MyResult<()> {
255        let mut walker = WalkDir::new(abs_root);
256        if let Some(depth) = self.config.max_depth() {
257            walker = walker.max_depth(depth);
258        }
259        let filter = self.choose_filter(git_cache);
260        for entry in walker.into_iter().filter_entry(filter) {
261            match entry {
262                Ok(entry) => self.walk_entry(entry, function)?,
263                Err(error) => function(Err(MyError::from(error))),
264            }
265        }
266        Ok(())
267    }
268
269    fn get_entry(&self, path: &Path) -> MyResult<Rc<Box<dyn Entry>>> {
270        if let Some(entry) = self.zip_entries.borrow().get(path) {
271            Ok(Rc::clone(entry))
272        } else {
273            let entry = FileEntry::from_path(path)?;
274            Ok(Rc::new(entry))
275        }
276    }
277
278    fn read_sig(&self, entry: &dyn Entry) -> Option<Signature> {
279        entry.read_sig()
280    }
281
282    #[cfg(windows)]
283    fn read_version(&self, entry: &dyn Entry) -> Option<String> {
284        entry.read_version()
285    }
286
287    fn read_link(&self, entry: &dyn Entry) -> MyResult<Option<PathBuf>> {
288        entry.read_link()
289    }
290
291    #[cfg(unix)]
292    fn get_mask(&self, uid: uid_t, gid: gid_t) -> u32 {
293        if uid == self.my_uid {
294            OWNER_MASK
295        } else if self.my_gids.contains(&gid) {
296            GROUP_MASK
297        } else {
298            OTHER_MASK
299        }
300    }
301
302    #[cfg(unix)]
303    fn find_user(&self, uid: uid_t) -> Option<Rc<String>> {
304        self.user_names
305            .borrow_mut()
306            .entry(uid)
307            .or_insert_with_key(Self::get_uid_name)
308            .as_ref()
309            .map(Rc::clone)
310    }
311
312    #[cfg(unix)]
313    fn find_group(&self, gid: gid_t) -> Option<Rc<String>> {
314        self.group_names
315            .borrow_mut()
316            .entry(gid)
317            .or_insert_with_key(Self::get_gid_name)
318            .as_ref()
319            .map(Rc::clone)
320    }
321}
322
323#[cfg(test)]
324pub mod tests {
325    use crate::config::Config;
326    use crate::error::{MyError, MyResult};
327    use crate::fs::entry::{Entry, EntryResult};
328    use crate::fs::file::Signature;
329    use crate::fs::metadata::Metadata;
330    #[cfg(unix)]
331    use crate::fs::system::EXEC_MASK;
332    use crate::fs::system::{FileEntry, FileSystem, System};
333    use crate::git::cache::GitCache;
334    use pretty_assertions::assert_eq;
335    use std::collections::BTreeMap;
336    use std::path::{Path, PathBuf};
337    use std::rc::Rc;
338    #[cfg(unix)]
339    use uzers::{gid_t, uid_t};
340
341    #[test]
342    fn test_shows_hidden_directories_and_shows_contents() {
343        assert_eq!(true, FileSystem::recurse_hidden_files(None, false, &PathBuf::from("/tmp/test")));
344        assert_eq!(true, FileSystem::recurse_hidden_files(None, false, &PathBuf::from("/tmp/.test")));
345        assert_eq!(true, FileSystem::recurse_hidden_files(None, false, &PathBuf::from("/tmp/test/visible")));
346        assert_eq!(true, FileSystem::recurse_hidden_files(None, false, &PathBuf::from("/tmp/.test/visible")));
347        assert_eq!(true, FileSystem::recurse_hidden_files(None, false, &PathBuf::from("/tmp/test/visible/file")));
348        assert_eq!(true, FileSystem::recurse_hidden_files(None, false, &PathBuf::from("/tmp/.test/visible/file")));
349        assert_eq!(true, FileSystem::recurse_hidden_files(None, false, &PathBuf::from("/tmp/test/.hidden")));
350        assert_eq!(true, FileSystem::recurse_hidden_files(None, false, &PathBuf::from("/tmp/.test/.hidden")));
351        assert_eq!(true, FileSystem::recurse_hidden_files(None, false, &PathBuf::from("/tmp/test/.hidden/file")));
352        assert_eq!(true, FileSystem::recurse_hidden_files(None, false, &PathBuf::from("/tmp/.test/.hidden/file")));
353    }
354
355    #[test]
356    fn test_shows_hidden_directories_and_hides_contents() {
357        assert_eq!(true, FileSystem::include_hidden_files(None, false, 0, &PathBuf::from("/tmp/test")));
358        assert_eq!(true, FileSystem::include_hidden_files(None, false, 0, &PathBuf::from("/tmp/.test")));
359        assert_eq!(true, FileSystem::include_hidden_files(None, false, 1, &PathBuf::from("/tmp/test/visible")));
360        assert_eq!(true, FileSystem::include_hidden_files(None, false, 1, &PathBuf::from("/tmp/.test/visible")));
361        assert_eq!(true, FileSystem::include_hidden_files(None, false, 2, &PathBuf::from("/tmp/test/visible/file")));
362        assert_eq!(true, FileSystem::include_hidden_files(None, false, 2, &PathBuf::from("/tmp/.test/visible/file")));
363        assert_eq!(true, FileSystem::include_hidden_files(None, false, 1, &PathBuf::from("/tmp/test/.hidden")));
364        assert_eq!(true, FileSystem::include_hidden_files(None, false, 1, &PathBuf::from("/tmp/.test/.hidden")));
365        assert_eq!(false, FileSystem::include_hidden_files(None, false, 2, &PathBuf::from("/tmp/test/.hidden/file")));
366        assert_eq!(false, FileSystem::include_hidden_files(None, false, 2, &PathBuf::from("/tmp/.test/.hidden/file")));
367    }
368
369    #[test]
370    fn test_hides_hidden_directories_and_hides_contents() {
371        assert_eq!(true, FileSystem::exclude_hidden_files(None, false, 0, &PathBuf::from("/tmp/test")));
372        assert_eq!(true, FileSystem::exclude_hidden_files(None, false, 0, &PathBuf::from("/tmp/.test")));
373        assert_eq!(true, FileSystem::exclude_hidden_files(None, false, 1, &PathBuf::from("/tmp/test/visible")));
374        assert_eq!(true, FileSystem::exclude_hidden_files(None, false, 1, &PathBuf::from("/tmp/.test/visible")));
375        assert_eq!(true, FileSystem::exclude_hidden_files(None, false, 2, &PathBuf::from("/tmp/test/visible/file")));
376        assert_eq!(true, FileSystem::exclude_hidden_files(None, false, 2, &PathBuf::from("/tmp/.test/visible/file")));
377        assert_eq!(false, FileSystem::exclude_hidden_files(None, false, 1, &PathBuf::from("/tmp/test/.hidden")));
378        assert_eq!(false, FileSystem::exclude_hidden_files(None, false, 1, &PathBuf::from("/tmp/.test/.hidden")));
379        assert_eq!(true, FileSystem::exclude_hidden_files(None, false, 2, &PathBuf::from("/tmp/test/.hidden/file")));
380        assert_eq!(true, FileSystem::exclude_hidden_files(None, false, 2, &PathBuf::from("/tmp/.test/.hidden/file")));
381    }
382
383    #[test]
384    fn test_detects_hidden_names() {
385        assert_eq!(false, FileSystem::is_hidden_name(None));
386        assert_eq!(false, FileSystem::is_hidden_name(Some("")));
387        assert_eq!(false, FileSystem::is_hidden_name(Some("visible")));
388        assert_eq!(false, FileSystem::is_hidden_name(Some("visible__")));
389        assert_eq!(false, FileSystem::is_hidden_name(Some("_visible_")));
390        assert_eq!(false, FileSystem::is_hidden_name(Some("__visible")));
391        assert_eq!(true, FileSystem::is_hidden_name(Some(".hidden")));
392        assert_eq!(true, FileSystem::is_hidden_name(Some("__hidden__")));
393    }
394
395    pub struct MockSystem<'a> {
396        config: &'a Config,
397        current: PathBuf,
398        entries: BTreeMap<PathBuf, FileEntry>,
399        links: BTreeMap<PathBuf, PathBuf>,
400        #[cfg(unix)]
401        user_names: BTreeMap<uid_t, String>,
402        #[cfg(unix)]
403        group_names: BTreeMap<gid_t, String>,
404    }
405
406    impl<'a> MockSystem<'a> {
407        pub fn new(
408            config: &'a Config,
409            current: PathBuf,
410            #[cfg(unix)]
411            user_names: BTreeMap<uid_t, String>,
412            #[cfg(unix)]
413            group_names: BTreeMap<uid_t, String>,
414        ) -> Self {
415            let entries = BTreeMap::new();
416            let links = BTreeMap::new();
417            Self {
418                config,
419                current,
420                entries,
421                links,
422                #[cfg(unix)]
423                user_names,
424                #[cfg(unix)]
425                group_names,
426            }
427        }
428
429        pub fn insert_entry(
430            &mut self,
431            file_depth: usize,
432            file_type: char,
433            file_mode: u32,
434            owner_uid: u32, // uid_t
435            owner_gid: u32, // gid_t
436            file_size: u64,
437            file_year: i32,
438            file_month: u32,
439            file_day: u32,
440            file_path: &str,
441            link_path: Option<&str>,
442        ) {
443            let file_path = self.current.join(file_path);
444            let metadata = Metadata::from_fields(
445                file_type,
446                file_mode,
447                owner_uid,
448                owner_gid,
449                file_size,
450                file_year,
451                file_month,
452                file_day,
453            );
454            let entry = FileEntry::from_fields(
455                file_path.clone(),
456                file_depth,
457                file_type,
458                metadata.clone(),
459            );
460            self.entries.insert(file_path.clone(), entry);
461            if let Some(link_path) = link_path {
462                let link_path = PathBuf::from(link_path);
463                self.links.insert(file_path, link_path);
464            }
465        }
466
467        fn filter_depth(&self, entry: &FileEntry) -> bool {
468            match self.config.max_depth() {
469                Some(depth) => entry.file_depth() <= depth,
470                None => true,
471            }
472        }
473    }
474
475    impl<'a> System for MockSystem<'a> {
476        fn walk_entries<F: Fn(EntryResult)>(
477            &self,
478            abs_root: &Path,
479            rel_root: &Path,
480            _git_cache: Option<Rc<GitCache>>,
481            function: &F,
482        ) -> MyResult<()> {
483            let rel_depth = rel_root.components().count();
484            for (_, entry) in self.entries.iter() {
485                if let Some(entry) = entry.subtract_depth(rel_depth) {
486                    if self.filter_depth(&entry) && entry.file_path().starts_with(abs_root) {
487                        function(Ok(&entry));
488                    }
489                }
490            }
491            Ok(())
492        }
493
494        fn get_entry(&self, path: &Path) -> MyResult<Rc<Box<dyn Entry>>> {
495            let entry = self.entries
496                .get(path)
497                .map(|entry| entry.clone())
498                .ok_or(MyError::Text(format!("Entry not found: {}", path.display())))?;
499            Ok(Rc::new(Box::new(entry)))
500        }
501
502        fn read_sig(&self, _entry: &dyn Entry) -> Option<Signature> {
503            None
504        }
505
506        #[cfg(windows)]
507        fn read_version(&self, _entry: &dyn Entry) -> Option<String> {
508            None
509        }
510
511        fn read_link(&self, entry: &dyn Entry) -> MyResult<Option<PathBuf>> {
512            let path = entry.file_path();
513            match self.links.get(path) {
514                Some(link) => Ok(Some(link.clone())),
515                None => Err(MyError::Text(format!("Link not found: {}", path.display()))),
516            }
517        }
518
519        #[cfg(unix)]
520        fn get_mask(&self, _uid: uid_t, _gid: gid_t) -> u32 {
521            EXEC_MASK
522        }
523
524        #[cfg(unix)]
525        fn find_user(&self, uid: uid_t) -> Option<Rc<String>> {
526            self.user_names.get(&uid).map(String::clone).map(Rc::new)
527        }
528
529        #[cfg(unix)]
530        fn find_group(&self, gid: gid_t) -> Option<Rc<String>> {
531            self.group_names.get(&gid).map(String::clone).map(Rc::new)
532        }
533    }
534}