ex_cli/fs/
system.rs

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