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 > 0 {
143            if is_dir && Self::is_ignored_dir(git_cache, path) {
144                return false;
145            }
146        }
147        if depth > 1 {
148            if let Some(parent) = path.parent() {
149                if let Some(name) = parent.file_name() {
150                    if Self::is_hidden_name(name.to_str()) {
151                        return false;
152                    }
153                }
154            }
155        }
156        true
157    }
158
159    fn exclude_hidden_files(
160        git_cache: Option<Rc<GitCache>>,
161        is_dir: bool,
162        depth: usize,
163        path: &Path,
164    ) -> bool {
165        if depth > 0 {
166            if is_dir && Self::is_ignored_dir(git_cache, path) {
167                return false;
168            }
169            let name = path.file_name().unwrap_or_else(|| path.as_os_str());
170            if Self::is_hidden_name(name.to_str()) {
171                return false;
172            }
173        }
174        true
175    }
176
177    fn is_ignored_dir(git_cache: Option<Rc<GitCache>>, path: &Path) -> bool {
178        if let Some(git_cache) = git_cache {
179            git_cache.test_ignored(path)
180        } else {
181            false
182        }
183    }
184
185    pub fn is_hidden_name(name: Option<&str>) -> bool {
186        if let Some(name) = name {
187            if name.starts_with(".") {
188                return true;
189            }
190            if name.starts_with("__") && name.ends_with("__") {
191                return true;
192            }
193        }
194        false
195    }
196
197    fn walk_entry<F: Fn(EntryResult)>(&self, entry: DirEntry, function: &F) -> MyResult<()> {
198        let zip_expand = self.config.zip_expand() && entry.file_type().is_file();
199        if let Some(zip_kind) = ZipKind::from_path(entry.path(), zip_expand) {
200            let mut zip_manager = self.zip_manager.borrow_mut();
201            zip_kind.walk_entries(self.config, &entry, &mut zip_manager, &|result| {
202                match result {
203                    Ok(entry) => {
204                        self.clone_entry(entry);
205                        function(Ok(entry));
206                    }
207                    Err(error) => {
208                        function(Err(error));
209                    }
210                }
211            })?;
212            let entry = FileEntry::from_entry(entry, true);
213            self.clone_entry(entry.as_ref());
214            function(Ok(entry.as_ref()));
215        } else {
216            let entry = FileEntry::from_entry(entry, false);
217            function(Ok(entry.as_ref()));
218        }
219        Ok(())
220    }
221
222    fn clone_entry(&self, entry: &dyn Entry) {
223        let path = PathBuf::from(entry.file_path());
224        let entry = CloneEntry::from_entry(entry);
225        self.zip_entries.borrow_mut().insert(path, entry);
226    }
227
228    #[cfg(unix)]
229    fn get_uid_name(uid: &uid_t) -> Option<Rc<String>> {
230        uzers::get_user_by_uid(*uid)
231            .as_ref()
232            .map(User::name)
233            .and_then(OsStr::to_str)
234            .map(str::to_string)
235            .map(Rc::new)
236    }
237
238    #[cfg(unix)]
239    fn get_gid_name(gid: &gid_t) -> Option<Rc<String>> {
240        uzers::get_group_by_gid(*gid)
241            .as_ref()
242            .map(Group::name)
243            .and_then(OsStr::to_str)
244            .map(str::to_string)
245            .map(Rc::new)
246    }
247}
248
249impl<'a> System for FileSystem<'a> {
250    fn walk_entries<F: Fn(EntryResult)>(
251        &self,
252        abs_root: &Path,
253        _rel_root: &Path,
254        git_cache: Option<Rc<GitCache>>,
255        function: &F,
256    ) -> MyResult<()> {
257        let mut walker = WalkDir::new(abs_root);
258        if let Some(depth) = self.config.max_depth() {
259            walker = walker.max_depth(depth);
260        }
261        let filter = self.choose_filter(git_cache);
262        for entry in walker.into_iter().filter_entry(filter) {
263            match entry {
264                Ok(entry) => self.walk_entry(entry, function)?,
265                Err(error) => function(Err(MyError::from(error))),
266            }
267        }
268        Ok(())
269    }
270
271    fn get_entry(&self, path: &Path) -> MyResult<Rc<Box<dyn Entry>>> {
272        if let Some(entry) = self.zip_entries.borrow().get(path) {
273            Ok(Rc::clone(entry))
274        } else {
275            let entry = FileEntry::from_path(path)?;
276            Ok(Rc::new(entry))
277        }
278    }
279
280    fn read_sig(&self, entry: &dyn Entry) -> Option<Signature> {
281        entry.read_sig()
282    }
283
284    #[cfg(windows)]
285    fn read_version(&self, entry: &dyn Entry) -> Option<String> {
286        entry.read_version()
287    }
288
289    fn read_link(&self, entry: &dyn Entry) -> MyResult<Option<PathBuf>> {
290        entry.read_link()
291    }
292
293    #[cfg(unix)]
294    fn get_mask(&self, uid: uid_t, gid: gid_t) -> u32 {
295        if uid == self.my_uid {
296            OWNER_MASK
297        } else if self.my_gids.contains(&gid) {
298            GROUP_MASK
299        } else {
300            OTHER_MASK
301        }
302    }
303
304    #[cfg(unix)]
305    fn find_user(&self, uid: uid_t) -> Option<Rc<String>> {
306        self.user_names
307            .borrow_mut()
308            .entry(uid)
309            .or_insert_with_key(Self::get_uid_name)
310            .as_ref()
311            .map(Rc::clone)
312    }
313
314    #[cfg(unix)]
315    fn find_group(&self, gid: gid_t) -> Option<Rc<String>> {
316        self.group_names
317            .borrow_mut()
318            .entry(gid)
319            .or_insert_with_key(Self::get_gid_name)
320            .as_ref()
321            .map(Rc::clone)
322    }
323}
324
325#[cfg(test)]
326pub mod tests {
327    use crate::config::Config;
328    use crate::error::{MyError, MyResult};
329    use crate::fs::entry::{Entry, EntryResult};
330    use crate::fs::file::Signature;
331    use crate::fs::metadata::Metadata;
332    #[cfg(unix)]
333    use crate::fs::system::EXEC_MASK;
334    use crate::fs::system::{FileEntry, FileSystem, System};
335    use crate::git::cache::GitCache;
336    use pretty_assertions::assert_eq;
337    use std::collections::BTreeMap;
338    use std::path::{Path, PathBuf};
339    use std::rc::Rc;
340    #[cfg(unix)]
341    use uzers::{gid_t, uid_t};
342
343    #[test]
344    fn test_shows_hidden_directories_and_shows_contents() {
345        assert_eq!(true, FileSystem::recurse_hidden_files(None, false, &PathBuf::from("/tmp/test")));
346        assert_eq!(true, FileSystem::recurse_hidden_files(None, false, &PathBuf::from("/tmp/.test")));
347        assert_eq!(true, FileSystem::recurse_hidden_files(None, false, &PathBuf::from("/tmp/test/visible")));
348        assert_eq!(true, FileSystem::recurse_hidden_files(None, false, &PathBuf::from("/tmp/.test/visible")));
349        assert_eq!(true, FileSystem::recurse_hidden_files(None, false, &PathBuf::from("/tmp/test/visible/file")));
350        assert_eq!(true, FileSystem::recurse_hidden_files(None, false, &PathBuf::from("/tmp/.test/visible/file")));
351        assert_eq!(true, FileSystem::recurse_hidden_files(None, false, &PathBuf::from("/tmp/test/.hidden")));
352        assert_eq!(true, FileSystem::recurse_hidden_files(None, false, &PathBuf::from("/tmp/.test/.hidden")));
353        assert_eq!(true, FileSystem::recurse_hidden_files(None, false, &PathBuf::from("/tmp/test/.hidden/file")));
354        assert_eq!(true, FileSystem::recurse_hidden_files(None, false, &PathBuf::from("/tmp/.test/.hidden/file")));
355    }
356
357    #[test]
358    fn test_shows_hidden_directories_and_hides_contents() {
359        assert_eq!(true, FileSystem::include_hidden_files(None, false, 0, &PathBuf::from("/tmp/test")));
360        assert_eq!(true, FileSystem::include_hidden_files(None, false, 0, &PathBuf::from("/tmp/.test")));
361        assert_eq!(true, FileSystem::include_hidden_files(None, false, 1, &PathBuf::from("/tmp/test/visible")));
362        assert_eq!(true, FileSystem::include_hidden_files(None, false, 1, &PathBuf::from("/tmp/.test/visible")));
363        assert_eq!(true, FileSystem::include_hidden_files(None, false, 2, &PathBuf::from("/tmp/test/visible/file")));
364        assert_eq!(true, FileSystem::include_hidden_files(None, false, 2, &PathBuf::from("/tmp/.test/visible/file")));
365        assert_eq!(true, FileSystem::include_hidden_files(None, false, 1, &PathBuf::from("/tmp/test/.hidden")));
366        assert_eq!(true, FileSystem::include_hidden_files(None, false, 1, &PathBuf::from("/tmp/.test/.hidden")));
367        assert_eq!(false, FileSystem::include_hidden_files(None, false, 2, &PathBuf::from("/tmp/test/.hidden/file")));
368        assert_eq!(false, FileSystem::include_hidden_files(None, false, 2, &PathBuf::from("/tmp/.test/.hidden/file")));
369    }
370
371    #[test]
372    fn test_hides_hidden_directories_and_hides_contents() {
373        assert_eq!(true, FileSystem::exclude_hidden_files(None, false, 0, &PathBuf::from("/tmp/test")));
374        assert_eq!(true, FileSystem::exclude_hidden_files(None, false, 0, &PathBuf::from("/tmp/.test")));
375        assert_eq!(true, FileSystem::exclude_hidden_files(None, false, 1, &PathBuf::from("/tmp/test/visible")));
376        assert_eq!(true, FileSystem::exclude_hidden_files(None, false, 1, &PathBuf::from("/tmp/.test/visible")));
377        assert_eq!(true, FileSystem::exclude_hidden_files(None, false, 2, &PathBuf::from("/tmp/test/visible/file")));
378        assert_eq!(true, FileSystem::exclude_hidden_files(None, false, 2, &PathBuf::from("/tmp/.test/visible/file")));
379        assert_eq!(false, FileSystem::exclude_hidden_files(None, false, 1, &PathBuf::from("/tmp/test/.hidden")));
380        assert_eq!(false, FileSystem::exclude_hidden_files(None, false, 1, &PathBuf::from("/tmp/.test/.hidden")));
381        assert_eq!(true, FileSystem::exclude_hidden_files(None, false, 2, &PathBuf::from("/tmp/test/.hidden/file")));
382        assert_eq!(true, FileSystem::exclude_hidden_files(None, false, 2, &PathBuf::from("/tmp/.test/.hidden/file")));
383    }
384
385    #[test]
386    fn test_detects_hidden_names() {
387        assert_eq!(false, FileSystem::is_hidden_name(None));
388        assert_eq!(false, FileSystem::is_hidden_name(Some("")));
389        assert_eq!(false, FileSystem::is_hidden_name(Some("visible")));
390        assert_eq!(false, FileSystem::is_hidden_name(Some("visible__")));
391        assert_eq!(false, FileSystem::is_hidden_name(Some("_visible_")));
392        assert_eq!(false, FileSystem::is_hidden_name(Some("__visible")));
393        assert_eq!(true, FileSystem::is_hidden_name(Some(".hidden")));
394        assert_eq!(true, FileSystem::is_hidden_name(Some("__hidden__")));
395    }
396
397    pub struct MockSystem<'a> {
398        config: &'a Config,
399        current: PathBuf,
400        entries: BTreeMap<PathBuf, FileEntry>,
401        links: BTreeMap<PathBuf, PathBuf>,
402        #[cfg(unix)]
403        user_names: BTreeMap<uid_t, String>,
404        #[cfg(unix)]
405        group_names: BTreeMap<gid_t, String>,
406    }
407
408    impl<'a> MockSystem<'a> {
409        pub fn new(
410            config: &'a Config,
411            current: PathBuf,
412            #[cfg(unix)]
413            user_names: BTreeMap<uid_t, String>,
414            #[cfg(unix)]
415            group_names: BTreeMap<uid_t, String>,
416        ) -> Self {
417            let entries = BTreeMap::new();
418            let links = BTreeMap::new();
419            Self {
420                config,
421                current,
422                entries,
423                links,
424                #[cfg(unix)]
425                user_names,
426                #[cfg(unix)]
427                group_names,
428            }
429        }
430
431        pub fn insert_entry(
432            &mut self,
433            file_depth: usize,
434            file_type: char,
435            file_mode: u32,
436            owner_uid: u32, // uid_t
437            owner_gid: u32, // gid_t
438            file_size: u64,
439            file_year: i32,
440            file_month: u32,
441            file_day: u32,
442            file_path: &str,
443            link_path: Option<&str>,
444        ) {
445            let file_path = self.current.join(file_path);
446            let metadata = Metadata::from_fields(
447                file_type,
448                file_mode,
449                owner_uid,
450                owner_gid,
451                file_size,
452                file_year,
453                file_month,
454                file_day,
455            );
456            let entry = FileEntry::from_fields(
457                file_path.clone(),
458                file_depth,
459                file_type,
460                metadata.clone(),
461            );
462            self.entries.insert(file_path.clone(), entry);
463            if let Some(link_path) = link_path {
464                let link_path = PathBuf::from(link_path);
465                self.links.insert(file_path, link_path);
466            }
467        }
468
469        fn filter_depth(&self, entry: &FileEntry) -> bool {
470            match self.config.max_depth() {
471                Some(depth) => entry.file_depth() <= depth,
472                None => true,
473            }
474        }
475    }
476
477    impl<'a> System for MockSystem<'a> {
478        fn walk_entries<F: Fn(EntryResult)>(
479            &self,
480            abs_root: &Path,
481            rel_root: &Path,
482            _git_cache: Option<Rc<GitCache>>,
483            function: &F,
484        ) -> MyResult<()> {
485            let rel_depth = rel_root.components().count();
486            for (_, entry) in self.entries.iter() {
487                if let Some(entry) = entry.subtract_depth(rel_depth) {
488                    if self.filter_depth(&entry) && entry.file_path().starts_with(abs_root) {
489                        function(Ok(&entry));
490                    }
491                }
492            }
493            Ok(())
494        }
495
496        fn get_entry(&self, path: &Path) -> MyResult<Rc<Box<dyn Entry>>> {
497            let entry = self.entries
498                .get(path)
499                .map(|entry| entry.clone())
500                .ok_or(MyError::Text(format!("Entry not found: {}", path.display())))?;
501            Ok(Rc::new(Box::new(entry)))
502        }
503
504        fn read_sig(&self, _entry: &dyn Entry) -> Option<Signature> {
505            None
506        }
507
508        #[cfg(windows)]
509        fn read_version(&self, _entry: &dyn Entry) -> Option<String> {
510            None
511        }
512
513        fn read_link(&self, entry: &dyn Entry) -> MyResult<Option<PathBuf>> {
514            let path = entry.file_path();
515            match self.links.get(path) {
516                Some(link) => Ok(Some(link.clone())),
517                None => Err(MyError::Text(format!("Link not found: {}", path.display()))),
518            }
519        }
520
521        #[cfg(unix)]
522        fn get_mask(&self, _uid: uid_t, _gid: gid_t) -> u32 {
523            EXEC_MASK
524        }
525
526        #[cfg(unix)]
527        fn find_user(&self, uid: uid_t) -> Option<Rc<String>> {
528            self.user_names.get(&uid).map(String::clone).map(Rc::new)
529        }
530
531        #[cfg(unix)]
532        fn find_group(&self, gid: gid_t) -> Option<Rc<String>> {
533            self.group_names.get(&gid).map(String::clone).map(Rc::new)
534        }
535    }
536}