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