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, owner_gid: u32, 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}