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}