Skip to main content

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