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
300            .borrow_mut()
301            .entry(uid)
302            .or_insert_with_key(Self::get_uid_name)
303            .as_ref()
304            .map(Rc::clone)
305    }
306
307    #[cfg(unix)]
308    fn find_group(&self, gid: gid_t) -> Option<Rc<String>> {
309        self.group_names
310            .borrow_mut()
311            .entry(gid)
312            .or_insert_with_key(Self::get_gid_name)
313            .as_ref()
314            .map(Rc::clone)
315    }
316}
317
318#[cfg(test)]
319pub mod tests {
320    use crate::config::Config;
321    use crate::error::{MyError, MyResult};
322    use crate::fs::entry::{Entry, EntryResult};
323    use crate::fs::file::Signature;
324    use crate::fs::metadata::Metadata;
325    #[cfg(unix)]
326    use crate::fs::system::EXEC_MASK;
327    use crate::fs::system::{FileEntry, FileSystem, System};
328    use crate::git::cache::GitCache;
329    use pretty_assertions::assert_eq;
330    use std::collections::BTreeMap;
331    use std::path::{Path, PathBuf};
332    use std::rc::Rc;
333    #[cfg(unix)]
334    use uzers::{gid_t, uid_t};
335
336    #[test]
337    fn test_shows_hidden_directories_and_shows_contents() {
338        assert_eq!(true, FileSystem::recurse_hidden_files(None, false, &PathBuf::from("/tmp/test")));
339        assert_eq!(true, FileSystem::recurse_hidden_files(None, false, &PathBuf::from("/tmp/.test")));
340        assert_eq!(true, FileSystem::recurse_hidden_files(None, false, &PathBuf::from("/tmp/test/visible")));
341        assert_eq!(true, FileSystem::recurse_hidden_files(None, false, &PathBuf::from("/tmp/.test/visible")));
342        assert_eq!(true, FileSystem::recurse_hidden_files(None, false, &PathBuf::from("/tmp/test/visible/file")));
343        assert_eq!(true, FileSystem::recurse_hidden_files(None, false, &PathBuf::from("/tmp/.test/visible/file")));
344        assert_eq!(true, FileSystem::recurse_hidden_files(None, false, &PathBuf::from("/tmp/test/.hidden")));
345        assert_eq!(true, FileSystem::recurse_hidden_files(None, false, &PathBuf::from("/tmp/.test/.hidden")));
346        assert_eq!(true, FileSystem::recurse_hidden_files(None, false, &PathBuf::from("/tmp/test/.hidden/file")));
347        assert_eq!(true, FileSystem::recurse_hidden_files(None, false, &PathBuf::from("/tmp/.test/.hidden/file")));
348    }
349
350    #[test]
351    fn test_shows_hidden_directories_and_hides_contents() {
352        assert_eq!(true, FileSystem::include_hidden_files(None, false, 0, &PathBuf::from("/tmp/test")));
353        assert_eq!(true, FileSystem::include_hidden_files(None, false, 0, &PathBuf::from("/tmp/.test")));
354        assert_eq!(true, FileSystem::include_hidden_files(None, false, 1, &PathBuf::from("/tmp/test/visible")));
355        assert_eq!(true, FileSystem::include_hidden_files(None, false, 1, &PathBuf::from("/tmp/.test/visible")));
356        assert_eq!(true, FileSystem::include_hidden_files(None, false, 2, &PathBuf::from("/tmp/test/visible/file")));
357        assert_eq!(true, FileSystem::include_hidden_files(None, false, 2, &PathBuf::from("/tmp/.test/visible/file")));
358        assert_eq!(true, FileSystem::include_hidden_files(None, false, 1, &PathBuf::from("/tmp/test/.hidden")));
359        assert_eq!(true, FileSystem::include_hidden_files(None, false, 1, &PathBuf::from("/tmp/.test/.hidden")));
360        assert_eq!(false, FileSystem::include_hidden_files(None, false, 2, &PathBuf::from("/tmp/test/.hidden/file")));
361        assert_eq!(false, FileSystem::include_hidden_files(None, false, 2, &PathBuf::from("/tmp/.test/.hidden/file")));
362    }
363
364    #[test]
365    fn test_hides_hidden_directories_and_hides_contents() {
366        assert_eq!(true, FileSystem::exclude_hidden_files(None, false, 0, &PathBuf::from("/tmp/test")));
367        assert_eq!(true, FileSystem::exclude_hidden_files(None, false, 0, &PathBuf::from("/tmp/.test")));
368        assert_eq!(true, FileSystem::exclude_hidden_files(None, false, 1, &PathBuf::from("/tmp/test/visible")));
369        assert_eq!(true, FileSystem::exclude_hidden_files(None, false, 1, &PathBuf::from("/tmp/.test/visible")));
370        assert_eq!(true, FileSystem::exclude_hidden_files(None, false, 2, &PathBuf::from("/tmp/test/visible/file")));
371        assert_eq!(true, FileSystem::exclude_hidden_files(None, false, 2, &PathBuf::from("/tmp/.test/visible/file")));
372        assert_eq!(false, FileSystem::exclude_hidden_files(None, false, 1, &PathBuf::from("/tmp/test/.hidden")));
373        assert_eq!(false, FileSystem::exclude_hidden_files(None, false, 1, &PathBuf::from("/tmp/.test/.hidden")));
374        assert_eq!(true, FileSystem::exclude_hidden_files(None, false, 2, &PathBuf::from("/tmp/test/.hidden/file")));
375        assert_eq!(true, FileSystem::exclude_hidden_files(None, false, 2, &PathBuf::from("/tmp/.test/.hidden/file")));
376    }
377
378    #[test]
379    fn test_detects_hidden_names() {
380        assert_eq!(false, FileSystem::is_hidden_name(None));
381        assert_eq!(false, FileSystem::is_hidden_name(Some("")));
382        assert_eq!(false, FileSystem::is_hidden_name(Some("visible")));
383        assert_eq!(false, FileSystem::is_hidden_name(Some("visible__")));
384        assert_eq!(false, FileSystem::is_hidden_name(Some("_visible_")));
385        assert_eq!(false, FileSystem::is_hidden_name(Some("__visible")));
386        assert_eq!(true, FileSystem::is_hidden_name(Some(".hidden")));
387        assert_eq!(true, FileSystem::is_hidden_name(Some("__hidden__")));
388    }
389
390    pub struct MockSystem<'a> {
391        config: &'a Config,
392        current: PathBuf,
393        entries: BTreeMap<PathBuf, FileEntry>,
394        links: BTreeMap<PathBuf, PathBuf>,
395        #[cfg(unix)]
396        user_names: BTreeMap<uid_t, String>,
397        #[cfg(unix)]
398        group_names: BTreeMap<gid_t, String>,
399    }
400
401    impl<'a> MockSystem<'a> {
402        pub fn new(
403            config: &'a Config,
404            current: PathBuf,
405            #[cfg(unix)]
406            user_names: BTreeMap<uid_t, String>,
407            #[cfg(unix)]
408            group_names: BTreeMap<uid_t, String>,
409        ) -> Self {
410            let entries = BTreeMap::new();
411            let links = BTreeMap::new();
412            Self {
413                config,
414                current,
415                entries,
416                links,
417                #[cfg(unix)]
418                user_names,
419                #[cfg(unix)]
420                group_names,
421            }
422        }
423
424        pub fn insert_entry(
425            &mut self,
426            file_depth: usize,
427            file_type: char,
428            file_mode: u32,
429            owner_uid: u32, // uid_t
430            owner_gid: u32, // gid_t
431            file_size: u64,
432            file_year: i32,
433            file_month: u32,
434            file_day: u32,
435            file_path: &str,
436            link_path: Option<&str>,
437        ) {
438            let file_path = self.current.join(file_path);
439            let metadata = Metadata::from_fields(
440                file_type,
441                file_mode,
442                owner_uid,
443                owner_gid,
444                file_size,
445                file_year,
446                file_month,
447                file_day,
448            );
449            let entry = FileEntry::from_fields(
450                file_path.clone(),
451                file_depth,
452                file_type,
453                metadata.clone(),
454            );
455            self.entries.insert(file_path.clone(), entry);
456            if let Some(link_path) = link_path {
457                let link_path = PathBuf::from(link_path);
458                self.links.insert(file_path, link_path);
459            }
460        }
461
462        fn filter_depth(&self, entry: &FileEntry) -> bool {
463            match self.config.max_depth {
464                Some(depth) => entry.file_depth() <= depth,
465                None => true,
466            }
467        }
468    }
469
470    impl<'a> System for MockSystem<'a> {
471        fn walk_entries<F: Fn(EntryResult)>(
472            &self,
473            abs_root: &Path,
474            rel_root: &Path,
475            _git_cache: Option<Rc<GitCache>>,
476            function: &F,
477        ) -> MyResult<()> {
478            let rel_depth = rel_root.components().count();
479            for (_, entry) in self.entries.iter() {
480                if let Some(entry) = entry.subtract_depth(rel_depth) {
481                    if self.filter_depth(&entry) && entry.file_path().starts_with(abs_root) {
482                        function(Ok(&entry));
483                    }
484                }
485            }
486            Ok(())
487        }
488
489        fn get_entry(&self, path: &Path) -> MyResult<Rc<Box<dyn Entry>>> {
490            let entry = self.entries
491                .get(path)
492                .map(|entry| entry.clone())
493                .ok_or(MyError::Text(format!("Entry not found: {}", path.display())))?;
494            Ok(Rc::new(Box::new(entry)))
495        }
496
497        fn read_sig(&self, _entry: &dyn Entry) -> Option<Signature> {
498            None
499        }
500
501        #[cfg(windows)]
502        fn read_version(&self, _entry: &dyn Entry) -> Option<String> {
503            None
504        }
505
506        fn read_link(&self, entry: &dyn Entry) -> MyResult<Option<PathBuf>> {
507            let path = entry.file_path();
508            match self.links.get(path) {
509                Some(link) => Ok(Some(link.clone())),
510                None => Err(MyError::Text(format!("Link not found: {}", path.display()))),
511            }
512        }
513
514        #[cfg(unix)]
515        fn get_mask(&self, _uid: uid_t, _gid: gid_t) -> u32 {
516            EXEC_MASK
517        }
518
519        #[cfg(unix)]
520        fn find_user(&self, uid: uid_t) -> Option<Rc<String>> {
521            self.user_names.get(&uid).map(String::clone).map(Rc::new)
522        }
523
524        #[cfg(unix)]
525        fn find_group(&self, gid: gid_t) -> Option<Rc<String>> {
526            self.group_names.get(&gid).map(String::clone).map(Rc::new)
527        }
528    }
529}