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