fs_tree/fs_tree.rs
1//! Implementation of [`FsTree`].
2
3use std::{
4 collections::BTreeMap,
5 ffi::OsStr,
6 io, mem,
7 ops::Index,
8 path::{Path, PathBuf},
9};
10
11use file_type_enum::FileType;
12
13use crate::{
14 iter::{Iter, NodesIter, PathsIter},
15 utils::{self, fs},
16 Error, Result,
17};
18
19/// The children [Trie](https://en.wikipedia.org/wiki/Trie) type alias.
20pub type TrieMap = BTreeMap<PathBuf, FsTree>;
21
22/// A filesystem tree recursive type.
23///
24/// # Iterators:
25///
26/// See the [iterator module documentation](crate::iter).
27#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
28pub enum FsTree {
29 /// A regular file.
30 Regular,
31 /// A directory, might have children `FsTree`s inside.
32 Directory(TrieMap),
33 /// Symbolic link, and it's target path (the link might be broken).
34 Symlink(PathBuf),
35}
36
37impl FsTree {
38 /// Creates an empty directory node.
39 ///
40 /// This is an alias to `FsTree::Directory(Default::default())`.
41 ///
42 /// ```
43 /// use fs_tree::{FsTree, TrieMap};
44 ///
45 /// let result = FsTree::new_dir();
46 /// let expected = FsTree::Directory(TrieMap::new());
47 ///
48 /// assert_eq!(result, expected);
49 /// ```
50 pub fn new_dir() -> Self {
51 Self::Directory(TrieMap::new())
52 }
53
54 /// Calculate the length by counting the leafs.
55 pub fn len_leafs(&self) -> usize {
56 if let Some(children) = self.children() {
57 children.values().map(Self::len_leafs).sum::<usize>()
58 } else if self.is_leaf() {
59 1
60 } else {
61 0
62 }
63 }
64
65 /// Calculate the length by counting all tree nodes, including the root.
66 pub fn len_all(&self) -> usize {
67 if let Some(children) = self.children() {
68 children.values().map(Self::len_leafs).sum::<usize>()
69 } else {
70 1
71 }
72 }
73
74 /// Construct a `FsTree` by reading from `path`, follows symlinks.
75 ///
76 /// If you want symlink-awareness, check [`symlink_read_at`].
77 ///
78 /// # Errors:
79 ///
80 /// - If any IO error occurs.
81 /// - If any file has an unexpected file type.
82 ///
83 /// [`symlink_read_at`]: FsTree::read_at
84 pub fn read_at(path: impl AsRef<Path>) -> Result<Self> {
85 Self::__read_at(path.as_ref(), true)
86 }
87
88 /// Construct a `FsTree` by reading from `path`.
89 ///
90 /// If you don't want symlink-awareness, check [`read_at`].
91 ///
92 /// # Errors:
93 ///
94 /// - If any IO error occurs.
95 /// - If any file has an unexpected file type.
96 ///
97 /// [`read_at`]: FsTree::symlink_read_at
98 pub fn symlink_read_at(path: impl AsRef<Path>) -> Result<Self> {
99 Self::__read_at(path.as_ref(), false)
100 }
101
102 fn __read_at(path: &Path, follow_symlinks: bool) -> Result<Self> {
103 let get_file_type = if follow_symlinks {
104 FileType::read_at
105 } else {
106 FileType::symlink_read_at
107 };
108
109 match get_file_type(path)? {
110 FileType::Regular => Ok(Self::Regular),
111 FileType::Directory => {
112 let mut children = TrieMap::new();
113
114 for entry in fs::read_dir(path)? {
115 let entry = entry?;
116 let entry_path = entry.path();
117
118 let node = Self::__read_at(&entry_path, follow_symlinks)?;
119
120 let stripped_file_path = entry_path
121 .strip_prefix(path)
122 .expect("Failed to strip prefix, expected to always succeed in Linux");
123
124 children.insert(stripped_file_path.into(), node);
125 }
126
127 Ok(Self::Directory(children))
128 },
129 FileType::Symlink => {
130 let target_path = utils::follow_symlink(path)?;
131 Ok(Self::Symlink(target_path))
132 },
133 other_type => {
134 Err(Error::UnexpectedFileTypeError(
135 other_type,
136 path.to_path_buf(),
137 ))
138 },
139 }
140 }
141
142 /// Construct a structural copy of this `FsTree` by reading files at the given path.
143 ///
144 /// In other words, the returned tree is formed of all paths in `self` that are also found in
145 /// the given `path` (intersection), missing files are skipped and types might differ.
146 ///
147 /// This function can be useful if you need to load a subtree from a huge folder and cannot
148 /// afford to load the whole folder, or if you just want to filter out every node outside of the
149 /// specified structure.
150 ///
151 /// This function will make at maximum `self.len()` syscalls.
152 ///
153 /// If you don't want symlink-awareness, check [`FsTree::symlink_read_structure_at`].
154 ///
155 /// # Examples:
156 ///
157 /// ```no_run
158 /// use fs_tree::FsTree;
159 ///
160 /// fn dynamically_load_structure() -> FsTree {
161 /// # "
162 /// ...
163 /// # "; todo!();
164 /// }
165 ///
166 /// let structure = dynamically_load_structure();
167 ///
168 /// let new_tree = structure.read_structure_at("path_here").unwrap();
169 ///
170 /// // It is guaranteed that every path in here is present in `structure`
171 /// for path in new_tree.paths() {
172 /// assert!(structure.get(path).is_some());
173 /// }
174 /// ```
175 ///
176 /// # Errors:
177 ///
178 /// - If an IO error happens, except [`io::ErrorKind::NotFound`]
179 ///
180 /// [`io::ErrorKind::NotFound`]: std::io::ErrorKind::NotFound
181 pub fn read_structure_at(&self, path: impl AsRef<Path>) -> Result<Self> {
182 self.__read_structure_at(path.as_ref(), true)
183 }
184
185 /// Construct a structural copy of this `FsTree` by reading files at the given path.
186 ///
187 /// In other words, the returned tree is formed of all paths in `self` that are also found in
188 /// the given `path` (intersection), missing files are skipped and types might differ.
189 ///
190 /// This function can be useful if you need to load a subtree from a huge folder and cannot
191 /// afford to load the whole folder, or if you just want to filter out every node outside of the
192 /// specified structure.
193 ///
194 /// This function will make at maximum `self.len()` syscalls.
195 ///
196 /// If you don't want symlink-awareness, check [`FsTree::read_structure_at`].
197 ///
198 /// # Examples:
199 ///
200 /// ```no_run
201 /// use fs_tree::FsTree;
202 ///
203 /// fn dynamically_load_structure() -> FsTree {
204 /// # "
205 /// ...
206 /// # "; todo!();
207 /// }
208 ///
209 /// let structure = dynamically_load_structure();
210 ///
211 /// let new_tree = structure.symlink_read_structure_at("path_here").unwrap();
212 ///
213 /// // It is guaranteed that every path in here is present in `structure`
214 /// for path in new_tree.paths() {
215 /// assert!(structure.get(path).is_some());
216 /// }
217 /// ```
218 ///
219 /// # Errors:
220 ///
221 /// - If an IO error happens, except [`io::ErrorKind::NotFound`]
222 ///
223 /// [`io::ErrorKind::NotFound`]: std::io::ErrorKind::NotFound
224 pub fn symlink_read_structure_at(&self, path: impl AsRef<Path>) -> Result<Self> {
225 self.__read_structure_at(path.as_ref(), false)
226 }
227
228 fn __read_structure_at(&self, folder: &Path, follow_symlinks: bool) -> Result<Self> {
229 let mut new_tree = FsTree::new_dir();
230
231 for relative_path in self.paths() {
232 // TODO: optimize this, instead of creating a PathBuf for each path,
233 // it's possible to use one mutable buffer with push + pop
234 let path = folder.join(&relative_path);
235
236 let get_file_type = if follow_symlinks {
237 FileType::read_at
238 } else {
239 FileType::symlink_read_at
240 };
241
242 let file_type = match get_file_type(&path) {
243 Ok(file_type) => file_type,
244 Err(err) if err.kind() == io::ErrorKind::NotFound => continue,
245 Err(err) => return Err(err.into()),
246 };
247
248 let node = match file_type {
249 FileType::Regular => Self::Regular,
250 FileType::Directory => Self::new_dir(),
251 FileType::Symlink => {
252 let target_path = utils::follow_symlink(&path)?;
253 Self::Symlink(target_path)
254 },
255 _ => continue,
256 };
257
258 new_tree.insert(relative_path, node);
259 }
260
261 Ok(new_tree)
262 }
263
264 /// Construct a `FsTree` from path pieces.
265 ///
266 /// Returns `None` if the input is empty.
267 ///
268 /// Returned value can correspond to a regular file or directory, but not a symlink.
269 ///
270 /// # Warning
271 ///
272 /// The last piece is always a file, so inputs ending with `/`, like `Path::new("example/")` are
273 /// **NOT** parsed as directories.
274 ///
275 /// For my usage cases it's OK, but open an issue if you think otherwise 👍.
276 ///
277 /// # Examples:
278 ///
279 /// ```
280 /// use fs_tree::{FsTree, tree};
281 ///
282 /// let result = FsTree::from_path_text("a/b/c");
283 ///
284 /// let expected = tree! {
285 /// a: {
286 /// b: {
287 /// c
288 /// }
289 /// }
290 /// };
291 ///
292 /// // The expected tree
293 /// assert_eq!(result, expected);
294 ///
295 /// // Nodes are nested
296 /// assert!(result.is_dir());
297 /// assert!(result["a"].is_dir());
298 /// assert!(result["a"]["b"].is_dir());
299 /// assert!(result["a"]["b"]["c"].is_regular());
300 /// ```
301 pub fn from_path_text(path: impl AsRef<Path>) -> Self {
302 Self::from_path_pieces(path.as_ref().iter())
303 }
304
305 /// Generic iterator version of [`from_path_text`](FsTree::from_path_text).
306 pub fn from_path_pieces<I, P>(path_iter: I) -> Self
307 where
308 I: IntoIterator<Item = P>,
309 P: Into<PathBuf>,
310 {
311 let mut path_iter = path_iter.into_iter();
312
313 if let Some(popped_piece) = path_iter.next() {
314 let child = (popped_piece.into(), Self::from_path_pieces(path_iter));
315 Self::Directory(TrieMap::from([child]))
316 } else {
317 Self::Regular
318 }
319 }
320
321 /// Creates an iterator that yields `(&FsTree, PathBuf)`.
322 ///
323 /// See iterator docs at the [`iter` module documentation](crate::iter).
324 pub fn iter(&self) -> Iter {
325 Iter::new(self)
326 }
327
328 /// Creates an iterator that yields `&FsTree`.
329 ///
330 /// See iterator docs at the [`iter` module documentation](crate::iter).
331 pub fn nodes(&self) -> NodesIter {
332 NodesIter::new(self)
333 }
334
335 /// Creates an iterator that yields `PathBuf`.
336 ///
337 /// See iterator docs at the [`iter` module documentation](crate::iter).
338 pub fn paths(&self) -> PathsIter {
339 PathsIter::new(self)
340 }
341
342 /// Returns `true` if `self` type matches `other` type.
343 pub fn is_same_type_as(&self, other: &Self) -> bool {
344 mem::discriminant(self) == mem::discriminant(other)
345 }
346
347 /// Returns `Ok(true)` if all nodes exist in the filesystem.
348 ///
349 /// # Errors:
350 ///
351 /// Similar to how [`Path::try_exists`] works, this function returns `false` if any IO error
352 /// occurred when checking [`std::fs::symlink_metadata`] (except [`io::ErrorKind::NotFound`]).
353 pub fn try_exists(&mut self) -> io::Result<bool> {
354 for path in self.paths() {
355 match fs::symlink_metadata(path) {
356 Ok(_) => continue,
357 Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(false),
358 Err(error) => return Err(error),
359 }
360 }
361
362 Ok(true)
363 }
364
365 /// Merge two trees.
366 ///
367 /// When conflicts happen, entries from `self` are kept, and the `other`'s are discarded.
368 pub fn merge(self, other: Self) -> Self {
369 // let's merge the right (consuming) onto the left (mutating)
370 let mut left = self;
371 let right = other;
372
373 match (&mut left, right) {
374 // both a directory at the same path, try merging
375 (FsTree::Directory(left_children), FsTree::Directory(right_children)) => {
376 for (path, right_node) in right_children {
377 // if right node exists, remove, merge and re-add, otherwise, just add it
378 if let Some(left_node) = left_children.remove(&path) {
379 let new_node = left_node.merge(right_node);
380 left_children.insert(path, new_node);
381 } else {
382 left_children.insert(path, right_node);
383 }
384 }
385 },
386 (_, _) => { /* conflict, but nothing to do, don't mutate left side */ },
387 }
388
389 left
390 }
391
392 /// Checks for conflicts in case the two trees would be merged.
393 ///
394 /// Also see [`Self::merge`].
395 pub fn conflicts_with(&self, other: &Self) -> bool {
396 let mut left = self;
397 let right = other;
398
399 match (&mut left, right) {
400 (FsTree::Directory(left_children), FsTree::Directory(right_children)) => {
401 for (path, right_node) in right_children {
402 if let Some(left_node) = left_children.get(path.as_path()) {
403 if left_node.conflicts_with(right_node) {
404 return true;
405 }
406 }
407 }
408 },
409 (_, _) => return true,
410 }
411
412 false
413 }
414
415 /// Reference to children if `self.is_directory()`.
416 pub fn children(&self) -> Option<&TrieMap> {
417 match &self {
418 Self::Directory(children) => Some(children),
419 _ => None,
420 }
421 }
422
423 /// Mutable reference to children if `self.is_directory()`.
424 pub fn children_mut(&mut self) -> Option<&mut TrieMap> {
425 match self {
426 Self::Directory(children) => Some(children),
427 _ => None,
428 }
429 }
430
431 /// Reference to target path, if `self.is_symlink()`.
432 pub fn target(&self) -> Option<&Path> {
433 match &self {
434 Self::Symlink(target_path) => Some(target_path),
435 _ => None,
436 }
437 }
438
439 /// Mutable reference to target path, if `self.is_symlink()`.
440 pub fn target_mut(&mut self) -> Option<&mut PathBuf> {
441 match self {
442 Self::Symlink(target_path) => Some(target_path),
443 _ => None,
444 }
445 }
446
447 // /// Apply a closure for each direct child of this FsTree.
448 // ///
449 // /// Only 1 level deep.
450 // pub fn apply_to_children0(&mut self, f: impl FnMut(&mut Self)) {
451 // if let Some(children) = self.children_mut() {
452 // children.iter_mut().for_each(f);
453 // }
454 // }
455
456 // /// Apply a closure to all direct and indirect descendants inside of this structure.
457 // ///
458 // /// Calls recursively for all levels.
459 // pub fn apply_to_all_children1(&mut self, f: impl FnMut(&mut Self) + Copy) {
460 // if let Some(children) = self.children_mut() {
461 // children
462 // .iter_mut()
463 // .for_each(|x| x.apply_to_all_children1(f));
464 // children.iter_mut().for_each(f);
465 // }
466 // }
467
468 // /// Apply a closure to all direct and indirect descendants inside (including root).
469 // ///
470 // /// Calls recursively for all levels.
471 // pub fn apply_to_all(&mut self, mut f: impl FnMut(&mut Self) + Copy) {
472 // f(self);
473 // if let Some(children) = self.children_mut() {
474 // for child in children.iter_mut() {
475 // child.apply_to_all(f);
476 // }
477 // }
478 // }
479
480 /// Returns `true` if `self` is a leaf node.
481 ///
482 /// A leaf node might be of any type, including directory, however, a
483 /// non-leaf node is always a directory.
484 pub fn is_leaf(&self) -> bool {
485 match self {
486 Self::Regular | Self::Symlink(_) => true,
487 Self::Directory(children) => children.is_empty(),
488 }
489 }
490
491 /// The variant string, useful for showing to user.
492 pub fn variant_str(&self) -> &'static str {
493 match self {
494 Self::Regular => "regular file",
495 Self::Directory(_) => "directory",
496 Self::Symlink(_) => "symlink",
497 }
498 }
499
500 /// Returns `true` if self matches the [`FsTree::Regular`] variant.
501 pub fn is_regular(&self) -> bool {
502 matches!(self, Self::Regular)
503 }
504
505 /// Returns `true` if self matches the [`FsTree::Directory`] variant.
506 pub fn is_dir(&self) -> bool {
507 matches!(self, Self::Directory(_))
508 }
509
510 /// Returns `true` if self matches the [`FsTree::Symlink`] variant.
511 pub fn is_symlink(&self) -> bool {
512 matches!(self, Self::Symlink(_))
513 }
514
515 // /// Generate a diff from two different trees.
516 // pub fn diff(&self, other: &Self) {
517 // if !self.has_same_type_as(other) {
518 // println!("Types differ! ");
519 // }
520
521 // let (self_children, other_children) = match (&self.file_type, &other.file_type) {
522 // (Self::Directory(self_children), Self::Directory(other_children)) => {
523 // (self_children, other_children)
524 // },
525 // _ => panic!(),
526 // };
527
528 // let mut lookup = self_children
529 // .iter()
530 // .map(|x| (&x.path, x))
531 // .collect::<HashMap<&PathBuf, &FsTree>>();
532
533 // for other_child in other_children {
534 // if let Some(self_child) = lookup.remove(&other_child.path) {
535 // if self_child.has_same_type_as(other_child) {
536 // if self_child.is_dir() {
537 // self_child.diff(other_child);
538 // }
539 // } else {
540 // println!(
541 // "File {:?} is a {} while file {:?} is a {}",
542 // self_child.path,
543 // self_child.file_type.file_type_display(),
544 // other_child.path,
545 // other_child.file_type.file_type_display(),
546 // );
547 // }
548 // } else {
549 // let path = &other_child.path;
550 // println!(
551 // "2Only in {:?}: {:?}",
552 // path.parent().unwrap(),
553 // path.file_name().unwrap()
554 // );
555 // }
556 // }
557
558 // for child_left in lookup.values() {
559 // let path = &child_left.path;
560 // println!(
561 // "1Only in {:?}: {:?}",
562 // path.parent().unwrap(),
563 // path.file_name().unwrap()
564 // );
565 // }
566 // }
567
568 /// Write the tree structure in the path.
569 ///
570 /// # Errors:
571 ///
572 /// - If provided folder doesn't exist, or is not a directory.
573 /// - If any other IO error occurs.
574 pub fn write_at(&self, folder: impl AsRef<Path>) -> Result<()> {
575 let folder = folder.as_ref();
576
577 #[cfg(feature = "fs-err")]
578 let symlink_function = fs_err::os::unix::fs::symlink;
579 #[cfg(not(feature = "fs-err"))]
580 let symlink_function = std::os::unix::fs::symlink;
581
582 for (node, path) in self.iter().skip(1) {
583 let path = folder.join(&path);
584
585 match &node {
586 Self::Regular => {
587 fs::File::create(path)?;
588 },
589 Self::Directory(_) => {
590 fs::create_dir(path)?;
591 },
592 Self::Symlink(target) => {
593 symlink_function(target, path)?;
594 },
595 }
596 }
597
598 Ok(())
599 }
600
601 /// Returns a reference to the node at the path, if any.
602 ///
603 /// # Errors:
604 ///
605 /// - Returns `None` if there is no node at the given path.
606 ///
607 /// # Examples:
608 ///
609 /// ```
610 /// use fs_tree::FsTree;
611 ///
612 /// let root = FsTree::from_path_text("a/b/c");
613 ///
614 /// // Indexing is relative from `root`, so `root` cannot be indexed.
615 /// assert_eq!(root, FsTree::from_path_text("a/b/c"));
616 /// assert_eq!(root["a"], FsTree::from_path_text("b/c"));
617 /// assert_eq!(root["a/b"], FsTree::from_path_text("c"));
618 /// assert_eq!(root["a"]["b"], FsTree::from_path_text("c"));
619 /// assert_eq!(root["a/b/c"], FsTree::Regular);
620 /// assert_eq!(root["a/b"]["c"], FsTree::Regular);
621 /// assert_eq!(root["a"]["b/c"], FsTree::Regular);
622 /// assert_eq!(root["a"]["b"]["c"], FsTree::Regular);
623 /// ```
624 pub fn get(&self, path: impl AsRef<Path>) -> Option<&Self> {
625 let path = path.as_ref();
626
627 // Split first piece from the rest
628 let (popped, path_rest) = {
629 let mut iter = path.iter();
630 let popped: Option<&Path> = iter.next().map(OsStr::as_ref);
631 (popped, iter.as_path())
632 };
633
634 // If path ended, we reached the desired node
635 let Some(popped) = popped else {
636 return Some(self);
637 };
638
639 // Corner case: if `.`, ignore it and call again with the rest
640 if popped == Path::new(".") {
641 return self.get(path_rest);
642 }
643
644 self.children()?
645 .get(popped)
646 .and_then(|child| child.get(path_rest))
647 }
648
649 /// Returns a mutable reference to the node at the path, if any.
650 ///
651 /// This is the mutable version of [`FsTree::get`].
652 pub fn get_mut(&mut self, path: impl AsRef<Path>) -> Option<&mut Self> {
653 let path = path.as_ref();
654
655 // Split first piece from the rest
656 let (popped, path_rest) = {
657 let mut iter = path.iter();
658 let popped: Option<&Path> = iter.next().map(OsStr::as_ref);
659 (popped, iter.as_path())
660 };
661
662 // If path ended, we reached the desired node
663 let Some(popped) = popped else {
664 return Some(self);
665 };
666
667 // Corner case: if `.`, ignore it and call again with the rest
668 if popped == Path::new(".") {
669 return self.get_mut(path_rest);
670 }
671
672 self.children_mut()?
673 .get_mut(popped)
674 .and_then(|child| child.get_mut(path_rest))
675 }
676
677 /// Inserts a node at the given path.
678 ///
679 /// # Panics:
680 ///
681 /// - If there are no directories up to the path node in order to insert it.
682 /// - If path is empty.
683 pub fn insert(&mut self, path: impl AsRef<Path>, node: Self) {
684 use FsTree::*;
685
686 let mut iter = path.as_ref().iter();
687
688 let Some(node_name) = iter.next_back().map(Path::new) else {
689 *self = node;
690 return;
691 };
692
693 let mut tree = self;
694
695 // Traverse tree
696 for next in iter {
697 // Give a better error message than the one below
698 if !tree.is_dir() {
699 panic!(
700 "Failed to insert node, while traversing, one of the parent directories \
701 ({next:?}) isn't a directory, but a {}",
702 tree.variant_str()
703 );
704 }
705
706 tree = if let Some(tree) = tree.get_mut(next) {
707 tree
708 } else {
709 panic!("Failed to insert node, parent directory {next:?} doesn't exist");
710 };
711 }
712
713 match tree {
714 Regular | Symlink(_) => {
715 panic!(
716 "Failed to insert node, parent directory is not a directory, but a {}",
717 tree.variant_str(),
718 );
719 },
720 Directory(children) => {
721 children.insert(node_name.into(), node);
722 },
723 }
724 }
725}
726
727#[cfg(feature = "libc-file-type")]
728impl FsTree {
729 /// Returns the file type equivalent [`libc::mode_t`] value.
730 pub fn as_mode_t(&self) -> libc::mode_t {
731 match self {
732 Self::Regular => libc::S_IFREG,
733 Self::Directory(_) => libc::S_IFDIR,
734 Self::Symlink(_) => libc::S_IFCHR,
735 }
736 }
737}
738
739impl<P> Index<P> for FsTree
740where
741 P: AsRef<Path>,
742{
743 type Output = FsTree;
744
745 fn index(&self, path: P) -> &Self::Output {
746 self.get(path.as_ref())
747 .unwrap_or_else(|| panic!("no node found for path '{}'", path.as_ref().display()))
748 }
749}
750
751#[cfg(test)]
752mod tests {
753 use std::{io, path::Path};
754
755 use pretty_assertions::{assert_eq, assert_ne};
756
757 use super::*;
758 use crate::tree;
759
760 fn testdir() -> io::Result<(tempfile::TempDir, &'static Path)> {
761 let dir = tempfile::tempdir()?;
762 let path = dir.path().to_path_buf().into_boxed_path();
763 Ok((dir, Box::leak(path)))
764 }
765
766 // #[test]
767 // fn test_diff() {
768 // let left = FsTree::from_path_text(".config/i3/file").unwrap();
769 // let right = FsTree::from_path_text(".config/i3/folder/file/oie").unwrap();
770 // left.diff(&right);
771 // panic!();
772 // }
773
774 #[test]
775 fn test_insert_basic() {
776 let mut tree = FsTree::new_dir();
777
778 let paths = ["a", "a/b", "a/b/c", "a/b/c/d", "a/b/c/d/e"];
779 for path in paths {
780 tree.insert(path, FsTree::new_dir());
781 }
782
783 tree.insert("a/b/c/d/e/f", FsTree::Regular);
784
785 let expected = tree! {
786 a: { b: { c: { d: { e: { f } } } } }
787 };
788
789 assert_eq!(tree, expected);
790 }
791
792 #[rustfmt::skip]
793 #[test]
794 fn test_insert_complete() {
795 let result = {
796 let mut tree = FsTree::new_dir();
797 tree.insert("config1", FsTree::Regular);
798 tree.insert("config2", FsTree::Regular);
799 tree.insert("outer_dir", FsTree::new_dir());
800 tree.insert("outer_dir/file1", FsTree::Regular);
801 tree.insert("outer_dir/file2", FsTree::Regular);
802 tree.insert("outer_dir/inner_dir", FsTree::new_dir());
803 tree.insert("outer_dir/inner_dir/inner1", FsTree::Regular);
804 tree.insert("outer_dir/inner_dir/inner2", FsTree::Regular);
805 tree.insert("outer_dir/inner_dir/inner3", FsTree::Regular);
806 tree.insert("outer_dir/inner_dir/inner_link", FsTree::Symlink("inner_target".into()));
807 tree.insert("link", FsTree::Symlink("target".into()));
808 tree.insert("config3", FsTree::Regular);
809 tree
810 };
811
812 let expected = tree! {
813 config1
814 config2
815 outer_dir: {
816 file1
817 file2
818 inner_dir: {
819 inner1
820 inner2
821 inner3
822 inner_link -> inner_target
823 }
824 }
825 link -> target
826 config3
827 };
828
829 assert_eq!(result, expected);
830 }
831
832 #[test]
833 fn test_write_at() {
834 let (_dropper, test_dir) = testdir().unwrap();
835
836 let tree = tree! {
837 a: {
838 b: {
839 c
840 empty: {}
841 link -> target
842 }
843 }
844 };
845
846 tree.write_at(test_dir).unwrap();
847
848 let result = FsTree::symlink_read_at(test_dir).unwrap();
849
850 assert_eq!(result, tree);
851 }
852
853 #[test]
854 fn test_get() {
855 let tree = FsTree::from_path_text("a/b/c");
856
857 assert_eq!(tree["a"], FsTree::from_path_text("b/c"));
858 assert_eq!(tree["a/b"], FsTree::from_path_text("c"));
859 assert_eq!(tree["a"]["b"], FsTree::from_path_text("c"));
860 assert_eq!(tree["a/b/c"], FsTree::Regular);
861 assert_eq!(tree["a/b"]["c"], FsTree::Regular);
862 assert_eq!(tree["a"]["b/c"], FsTree::Regular);
863 assert_eq!(tree["a"]["b"]["c"], FsTree::Regular);
864
865 // Paths are relative, so empty path returns the node itself
866 assert_eq!(tree[""], tree);
867 assert_eq!(tree[""], tree[""]);
868
869 // "."s are ignored
870 assert_eq!(tree["."], tree[""]);
871 assert_eq!(tree["././"], tree["."]);
872 assert_eq!(tree["././."], tree);
873 assert_eq!(tree["./a/."]["././b/./."], FsTree::from_path_text("c"));
874 assert_eq!(tree["./a/./b"]["c/."], FsTree::Regular);
875 }
876
877 // #[test]
878 // fn test_simple_merge() {
879 // let left = FsTree::from_path_text(".config/i3/file");
880 // let right = FsTree::from_path_text(".config/i3/folder/file");
881 // let result = left.try_merge(right);
882
883 // let expected = tree! {
884 // ".config": {
885 // i3: {
886 // file
887 // folder: {
888 // file
889 // }
890 // }
891 // }
892 // };
893
894 // assert_eq!(result, Some(expected));
895 // }
896
897 #[test]
898 fn test_partial_eq_fails() {
899 let left = FsTree::from_path_text(".config/i3/a");
900 let right = FsTree::from_path_text(".config/i3/b");
901
902 assert_ne!(left, right);
903 }
904}