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