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