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