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.borrow_mut()
300 .entry(uid)
301 .or_insert_with_key(Self::get_uid_name)
302 .as_ref()
303 .map(Rc::clone)
304 }
305
306 #[cfg(unix)]
307 fn find_group(&self, gid: gid_t) -> Option<Rc<String>> {
308 self.group_names.borrow_mut()
309 .entry(gid)
310 .or_insert_with_key(Self::get_gid_name)
311 .as_ref()
312 .map(Rc::clone)
313 }
314}
315
316#[cfg(test)]
317pub mod tests {
318 use crate::config::Config;
319 use crate::error::{MyError, MyResult};
320 use crate::fs::entry::{Entry, EntryResult};
321 use crate::fs::file::Signature;
322 use crate::fs::metadata::Metadata;
323 #[cfg(unix)]
324 use crate::fs::system::EXEC_MASK;
325 use crate::fs::system::{FileEntry, FileSystem, System};
326 use crate::git::cache::GitCache;
327 use pretty_assertions::assert_eq;
328 use std::collections::BTreeMap;
329 use std::path::{Path, PathBuf};
330 use std::rc::Rc;
331 #[cfg(unix)]
332 use uzers::{gid_t, uid_t};
333
334 #[test]
335 fn test_shows_hidden_directories_and_shows_contents() {
336 assert_eq!(true, FileSystem::recurse_hidden_files(None, false, &PathBuf::from("/tmp/test")));
337 assert_eq!(true, FileSystem::recurse_hidden_files(None, false, &PathBuf::from("/tmp/.test")));
338 assert_eq!(true, FileSystem::recurse_hidden_files(None, false, &PathBuf::from("/tmp/test/visible")));
339 assert_eq!(true, FileSystem::recurse_hidden_files(None, false, &PathBuf::from("/tmp/.test/visible")));
340 assert_eq!(true, FileSystem::recurse_hidden_files(None, false, &PathBuf::from("/tmp/test/visible/file")));
341 assert_eq!(true, FileSystem::recurse_hidden_files(None, false, &PathBuf::from("/tmp/.test/visible/file")));
342 assert_eq!(true, FileSystem::recurse_hidden_files(None, false, &PathBuf::from("/tmp/test/.hidden")));
343 assert_eq!(true, FileSystem::recurse_hidden_files(None, false, &PathBuf::from("/tmp/.test/.hidden")));
344 assert_eq!(true, FileSystem::recurse_hidden_files(None, false, &PathBuf::from("/tmp/test/.hidden/file")));
345 assert_eq!(true, FileSystem::recurse_hidden_files(None, false, &PathBuf::from("/tmp/.test/.hidden/file")));
346 }
347
348 #[test]
349 fn test_shows_hidden_directories_and_hides_contents() {
350 assert_eq!(true, FileSystem::include_hidden_files(None, false, 0, &PathBuf::from("/tmp/test")));
351 assert_eq!(true, FileSystem::include_hidden_files(None, false, 0, &PathBuf::from("/tmp/.test")));
352 assert_eq!(true, FileSystem::include_hidden_files(None, false, 1, &PathBuf::from("/tmp/test/visible")));
353 assert_eq!(true, FileSystem::include_hidden_files(None, false, 1, &PathBuf::from("/tmp/.test/visible")));
354 assert_eq!(true, FileSystem::include_hidden_files(None, false, 2, &PathBuf::from("/tmp/test/visible/file")));
355 assert_eq!(true, FileSystem::include_hidden_files(None, false, 2, &PathBuf::from("/tmp/.test/visible/file")));
356 assert_eq!(true, FileSystem::include_hidden_files(None, false, 1, &PathBuf::from("/tmp/test/.hidden")));
357 assert_eq!(true, FileSystem::include_hidden_files(None, false, 1, &PathBuf::from("/tmp/.test/.hidden")));
358 assert_eq!(false, FileSystem::include_hidden_files(None, false, 2, &PathBuf::from("/tmp/test/.hidden/file")));
359 assert_eq!(false, FileSystem::include_hidden_files(None, false, 2, &PathBuf::from("/tmp/.test/.hidden/file")));
360 }
361
362 #[test]
363 fn test_hides_hidden_directories_and_hides_contents() {
364 assert_eq!(true, FileSystem::exclude_hidden_files(None, false, 0, &PathBuf::from("/tmp/test")));
365 assert_eq!(true, FileSystem::exclude_hidden_files(None, false, 0, &PathBuf::from("/tmp/.test")));
366 assert_eq!(true, FileSystem::exclude_hidden_files(None, false, 1, &PathBuf::from("/tmp/test/visible")));
367 assert_eq!(true, FileSystem::exclude_hidden_files(None, false, 1, &PathBuf::from("/tmp/.test/visible")));
368 assert_eq!(true, FileSystem::exclude_hidden_files(None, false, 2, &PathBuf::from("/tmp/test/visible/file")));
369 assert_eq!(true, FileSystem::exclude_hidden_files(None, false, 2, &PathBuf::from("/tmp/.test/visible/file")));
370 assert_eq!(false, FileSystem::exclude_hidden_files(None, false, 1, &PathBuf::from("/tmp/test/.hidden")));
371 assert_eq!(false, FileSystem::exclude_hidden_files(None, false, 1, &PathBuf::from("/tmp/.test/.hidden")));
372 assert_eq!(true, FileSystem::exclude_hidden_files(None, false, 2, &PathBuf::from("/tmp/test/.hidden/file")));
373 assert_eq!(true, FileSystem::exclude_hidden_files(None, false, 2, &PathBuf::from("/tmp/.test/.hidden/file")));
374 }
375
376 #[test]
377 fn test_detects_hidden_names() {
378 assert_eq!(false, FileSystem::is_hidden_name(None));
379 assert_eq!(false, FileSystem::is_hidden_name(Some("")));
380 assert_eq!(false, FileSystem::is_hidden_name(Some("visible")));
381 assert_eq!(false, FileSystem::is_hidden_name(Some("visible__")));
382 assert_eq!(false, FileSystem::is_hidden_name(Some("_visible_")));
383 assert_eq!(false, FileSystem::is_hidden_name(Some("__visible")));
384 assert_eq!(true, FileSystem::is_hidden_name(Some(".hidden")));
385 assert_eq!(true, FileSystem::is_hidden_name(Some("__hidden__")));
386 }
387
388 pub struct MockSystem<'a> {
389 config: &'a Config,
390 current: PathBuf,
391 entries: BTreeMap<PathBuf, FileEntry>,
392 links: BTreeMap<PathBuf, PathBuf>,
393 #[cfg(unix)]
394 user_names: BTreeMap<uid_t, String>,
395 #[cfg(unix)]
396 group_names: BTreeMap<gid_t, String>,
397 }
398
399 impl<'a> MockSystem<'a> {
400 pub fn new(
401 config: &'a Config,
402 current: PathBuf,
403 #[cfg(unix)]
404 user_names: BTreeMap<uid_t, String>,
405 #[cfg(unix)]
406 group_names: BTreeMap<uid_t, String>,
407 ) -> Self {
408 let entries = BTreeMap::new();
409 let links = BTreeMap::new();
410 Self {
411 config,
412 current,
413 entries,
414 links,
415 #[cfg(unix)]
416 user_names,
417 #[cfg(unix)]
418 group_names,
419 }
420 }
421
422 pub fn insert_entry(
423 &mut self,
424 file_depth: usize,
425 file_type: char,
426 file_mode: u32,
427 owner_uid: u32, owner_gid: u32, file_size: u64,
430 file_year: i32,
431 file_month: u32,
432 file_day: u32,
433 file_path: &str,
434 link_path: Option<&str>,
435 ) {
436 let file_path = self.current.join(file_path);
437 let metadata = Metadata::from_fields(
438 file_type,
439 file_mode,
440 owner_uid,
441 owner_gid,
442 file_size,
443 file_year,
444 file_month,
445 file_day,
446 );
447 let entry = FileEntry::from_fields(
448 file_path.clone(),
449 file_depth,
450 file_type,
451 metadata.clone(),
452 );
453 self.entries.insert(file_path.clone(), entry);
454 if let Some(link_path) = link_path {
455 let link_path = PathBuf::from(link_path);
456 self.links.insert(file_path, link_path);
457 }
458 }
459
460 fn filter_depth(&self, entry: &FileEntry) -> bool {
461 match self.config.max_depth {
462 Some(depth) => entry.file_depth() <= depth,
463 None => true,
464 }
465 }
466 }
467
468 impl<'a> System for MockSystem<'a> {
469 fn walk_entries<F: Fn(EntryResult)>(
470 &self,
471 abs_root: &Path,
472 rel_root: &Path,
473 _git_cache: Option<Rc<GitCache>>,
474 function: &F,
475 ) -> MyResult<()> {
476 let rel_depth = rel_root.components().count();
477 for (_, entry) in self.entries.iter() {
478 if let Some(entry) = entry.subtract_depth(rel_depth) {
479 if self.filter_depth(&entry) && entry.file_path().starts_with(abs_root) {
480 function(Ok(&entry));
481 }
482 }
483 }
484 Ok(())
485 }
486
487 fn get_entry(&self, path: &Path) -> MyResult<Rc<Box<dyn Entry>>> {
488 let entry = self.entries.get(path)
489 .map(|entry| entry.clone())
490 .ok_or(MyError::Text(format!("Entry not found: {}", path.display())))?;
491 Ok(Rc::new(Box::new(entry)))
492 }
493
494 fn read_sig(&self, _entry: &dyn Entry) -> Option<Signature> {
495 None
496 }
497
498 #[cfg(windows)]
499 fn read_version(&self, _entry: &dyn Entry) -> Option<String> {
500 None
501 }
502
503 fn read_link(&self, entry: &dyn Entry) -> MyResult<Option<PathBuf>> {
504 let path = entry.file_path();
505 match self.links.get(path) {
506 Some(link) => Ok(Some(link.clone())),
507 None => Err(MyError::Text(format!("Link not found: {}", path.display()))),
508 }
509 }
510
511 #[cfg(unix)]
512 fn get_mask(&self, _uid: uid_t, _gid: gid_t) -> u32 {
513 EXEC_MASK
514 }
515
516 #[cfg(unix)]
517 fn find_user(&self, uid: uid_t) -> Option<Rc<String>> {
518 self.user_names.get(&uid).map(String::clone).map(Rc::new)
519 }
520
521 #[cfg(unix)]
522 fn find_group(&self, gid: gid_t) -> Option<Rc<String>> {
523 self.group_names.get(&gid).map(String::clone).map(Rc::new)
524 }
525 }
526}