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 at the given folder path.
565    ///
566    /// This method creates files, directories, and symlinks as defined by the
567    /// tree structure. When a path already exists, the behavior depends on the
568    /// node type:
569    ///
570    /// - **Regular files**: If a regular file already exists at the path, it is
571    ///   left unchanged. If a non-regular file exists, returns
572    ///   [`Error::NotARegularFile`].
573    ///
574    /// - **Directories**: If a directory already exists at the path, it is left
575    ///   unchanged. If a non-directory exists, returns [`Error::NotADirectory`].
576    ///
577    /// - **Symlinks**: If a symlink already exists and points to the expected
578    ///   target, it is left unchanged. If a symlink exists but points to a
579    ///   different target, returns [`Error::SymlinkTargetMismatch`]. If a
580    ///   non-symlink exists, returns [`Error::NotASymlink`].
581    ///
582    /// # Errors
583    ///
584    /// - If the provided folder doesn't exist, or is not a directory.
585    /// - If a path conflict occurs (see above for conflict rules).
586    /// - If any other IO error occurs.
587    ///
588    /// [`Error::NotARegularFile`]: crate::Error::NotARegularFile
589    /// [`Error::NotADirectory`]: crate::Error::NotADirectory
590    /// [`Error::NotASymlink`]: crate::Error::NotASymlink
591    /// [`Error::SymlinkTargetMismatch`]: crate::Error::SymlinkTargetMismatch
592    pub fn write_structure_at(&self, folder: impl AsRef<Path>) -> Result<()> {
593        let folder = folder.as_ref();
594
595        #[cfg(feature = "fs-err")]
596        let symlink_function = fs_err::os::unix::fs::symlink;
597        #[cfg(not(feature = "fs-err"))]
598        let symlink_function = std::os::unix::fs::symlink;
599
600        for (node, relative_path) in self.iter().skip(1) {
601            let path = folder.join(&relative_path);
602
603            match &node {
604                Self::Regular => {
605                    if path.exists() {
606                        if !path.is_file() {
607                            return Err(Error::NotARegularFile(path));
608                        }
609                    } else {
610                        fs::File::create(path)?;
611                    }
612                },
613                Self::Directory(_) => {
614                    if path.exists() {
615                        if !path.is_dir() {
616                            return Err(Error::NotADirectory(path));
617                        }
618                    } else {
619                        fs::create_dir(path)?;
620                    }
621                },
622                Self::Symlink(expected_target) => {
623                    match FileType::symlink_read_at(&path) {
624                        Ok(file_type) if file_type.is_symlink() => {
625                            let actual_target = fs::read_link(&path)?;
626                            if actual_target != *expected_target {
627                                return Err(Error::SymlinkTargetMismatch {
628                                    path,
629                                    expected: expected_target.clone(),
630                                    found: actual_target,
631                                });
632                            }
633                        },
634                        Ok(_) => {
635                            return Err(Error::NotASymlink(path));
636                        },
637                        Err(_) => {
638                            symlink_function(expected_target, path)?;
639                        },
640                    }
641                },
642            }
643        }
644
645        Ok(())
646    }
647
648    /// Returns a reference to the node at the path, if any.
649    ///
650    /// # Errors:
651    ///
652    /// - Returns `None` if there is no node at the given path.
653    ///
654    /// # Examples:
655    ///
656    /// ```
657    /// use fs_tree::FsTree;
658    ///
659    /// let root = FsTree::from_path_text("a/b/c");
660    ///
661    /// // Indexing is relative from `root`, so `root` cannot be indexed.
662    /// assert_eq!(root, FsTree::from_path_text("a/b/c"));
663    /// assert_eq!(root["a"], FsTree::from_path_text("b/c"));
664    /// assert_eq!(root["a/b"], FsTree::from_path_text("c"));
665    /// assert_eq!(root["a"]["b"], FsTree::from_path_text("c"));
666    /// assert_eq!(root["a/b/c"], FsTree::Regular);
667    /// assert_eq!(root["a/b"]["c"], FsTree::Regular);
668    /// assert_eq!(root["a"]["b/c"], FsTree::Regular);
669    /// assert_eq!(root["a"]["b"]["c"], FsTree::Regular);
670    /// ```
671    pub fn get(&self, path: impl AsRef<Path>) -> Option<&Self> {
672        let path = path.as_ref();
673
674        // Split first piece from the rest
675        let (popped, path_rest) = {
676            let mut iter = path.iter();
677            let popped: Option<&Path> = iter.next().map(OsStr::as_ref);
678            (popped, iter.as_path())
679        };
680
681        // If path ended, we reached the desired node
682        let Some(popped) = popped else {
683            return Some(self);
684        };
685
686        // Corner case: if `.`, ignore it and call again with the rest
687        if popped == Path::new(".") {
688            return self.get(path_rest);
689        }
690
691        self.children()?
692            .get(popped)
693            .and_then(|child| child.get(path_rest))
694    }
695
696    /// Returns a mutable reference to the node at the path, if any.
697    ///
698    /// This is the mutable version of [`FsTree::get`].
699    pub fn get_mut(&mut self, path: impl AsRef<Path>) -> Option<&mut Self> {
700        let path = path.as_ref();
701
702        // Split first piece from the rest
703        let (popped, path_rest) = {
704            let mut iter = path.iter();
705            let popped: Option<&Path> = iter.next().map(OsStr::as_ref);
706            (popped, iter.as_path())
707        };
708
709        // If path ended, we reached the desired node
710        let Some(popped) = popped else {
711            return Some(self);
712        };
713
714        // Corner case: if `.`, ignore it and call again with the rest
715        if popped == Path::new(".") {
716            return self.get_mut(path_rest);
717        }
718
719        self.children_mut()?
720            .get_mut(popped)
721            .and_then(|child| child.get_mut(path_rest))
722    }
723
724    /// Inserts a node at the given path.
725    ///
726    /// # Panics:
727    ///
728    /// - If there are no directories up to the path node in order to insert it.
729    /// - If path is empty.
730    pub fn insert(&mut self, path: impl AsRef<Path>, node: Self) {
731        use FsTree::*;
732
733        let mut iter = path.as_ref().iter();
734
735        let Some(node_name) = iter.next_back().map(Path::new) else {
736            *self = node;
737            return;
738        };
739
740        let mut tree = self;
741
742        // Traverse tree
743        for next in iter {
744            // Give a better error message than the one below
745            if !tree.is_dir() {
746                panic!(
747                    "Failed to insert node, while traversing, one of the parent directories \
748                    ({next:?}) isn't a directory, but a {}",
749                    tree.variant_str()
750                );
751            }
752
753            tree = if let Some(tree) = tree.get_mut(next) {
754                tree
755            } else {
756                panic!("Failed to insert node, parent directory {next:?} doesn't exist");
757            };
758        }
759
760        match tree {
761            Regular | Symlink(_) => {
762                panic!(
763                    "Failed to insert node, parent directory is not a directory, but a {}",
764                    tree.variant_str(),
765                );
766            },
767            Directory(children) => {
768                children.insert(node_name.into(), node);
769            },
770        }
771    }
772}
773
774#[cfg(feature = "libc-file-type")]
775impl FsTree {
776    /// Returns the file type equivalent [`libc::mode_t`] value.
777    pub fn as_mode_t(&self) -> libc::mode_t {
778        match self {
779            Self::Regular => libc::S_IFREG,
780            Self::Directory(_) => libc::S_IFDIR,
781            Self::Symlink(_) => libc::S_IFCHR,
782        }
783    }
784}
785
786impl<P> Index<P> for FsTree
787where
788    P: AsRef<Path>,
789{
790    type Output = FsTree;
791
792    fn index(&self, path: P) -> &Self::Output {
793        self.get(path.as_ref())
794            .unwrap_or_else(|| panic!("no node found for path '{}'", path.as_ref().display()))
795    }
796}
797
798#[cfg(test)]
799mod tests {
800    use std::{io, path::Path};
801
802    use pretty_assertions::{assert_eq, assert_ne};
803
804    use super::*;
805    use crate::tree;
806
807    fn testdir() -> io::Result<(tempfile::TempDir, &'static Path)> {
808        let dir = tempfile::tempdir()?;
809        let path = dir.path().to_path_buf().into_boxed_path();
810        Ok((dir, Box::leak(path)))
811    }
812
813    #[test]
814    fn test_len_all_counts_all_nodes_including_root() {
815        let tree = tree! {
816            file1
817            dir1: [
818                file2
819                file3
820            ]
821            file4
822        };
823
824        assert_eq!(tree.len_all(), 6);
825    }
826
827    // #[test]
828    // fn test_diff() {
829    //     let left = FsTree::from_path_text(".config/i3/file").unwrap();
830    //     let right = FsTree::from_path_text(".config/i3/folder/file/oie").unwrap();
831    //     left.diff(&right);
832    //     panic!();
833    // }
834
835    #[test]
836    fn test_insert_basic() {
837        let mut tree = FsTree::new_dir();
838
839        let paths = ["a", "a/b", "a/b/c", "a/b/c/d", "a/b/c/d/e"];
840        for path in paths {
841            tree.insert(path, FsTree::new_dir());
842        }
843
844        tree.insert("a/b/c/d/e/f", FsTree::Regular);
845
846        let expected = tree! {
847            a: [ b: [ c: [ d: [ e: [ f ] ] ] ] ]
848        };
849
850        assert_eq!(tree, expected);
851    }
852
853    #[rustfmt::skip]
854    #[test]
855    fn test_insert_complete() {
856        let result = {
857            let mut tree = FsTree::new_dir();
858            tree.insert("config1", FsTree::Regular);
859            tree.insert("config2", FsTree::Regular);
860            tree.insert("outer_dir", FsTree::new_dir());
861            tree.insert("outer_dir/file1", FsTree::Regular);
862            tree.insert("outer_dir/file2", FsTree::Regular);
863            tree.insert("outer_dir/inner_dir", FsTree::new_dir());
864            tree.insert("outer_dir/inner_dir/inner1", FsTree::Regular);
865            tree.insert("outer_dir/inner_dir/inner2", FsTree::Regular);
866            tree.insert("outer_dir/inner_dir/inner3", FsTree::Regular);
867            tree.insert("outer_dir/inner_dir/inner_link", FsTree::Symlink("inner_target".into()));
868            tree.insert("link", FsTree::Symlink("target".into()));
869            tree.insert("config3", FsTree::Regular);
870            tree
871        };
872
873        let expected = tree! {
874            config1
875            config2
876            outer_dir: [
877                file1
878                file2
879                inner_dir: [
880                    inner1
881                    inner2
882                    inner3
883                    inner_link -> inner_target
884                ]
885            ]
886            link -> target
887            config3
888        };
889
890        assert_eq!(result, expected);
891    }
892
893    #[test]
894    fn test_write_structure_at() {
895        let (_dropper, test_dir) = testdir().unwrap();
896
897        let tree = tree! {
898            a: [
899                b: [
900                    c
901                    empty: []
902                    link -> target
903                ]
904            ]
905        };
906
907        tree.write_structure_at(test_dir).unwrap();
908
909        let result = FsTree::symlink_read_at(test_dir).unwrap();
910
911        assert_eq!(result, tree);
912    }
913
914    #[test]
915    fn test_get() {
916        let tree = FsTree::from_path_text("a/b/c");
917
918        assert_eq!(tree["a"], FsTree::from_path_text("b/c"));
919        assert_eq!(tree["a/b"], FsTree::from_path_text("c"));
920        assert_eq!(tree["a"]["b"], FsTree::from_path_text("c"));
921        assert_eq!(tree["a/b/c"], FsTree::Regular);
922        assert_eq!(tree["a/b"]["c"], FsTree::Regular);
923        assert_eq!(tree["a"]["b/c"], FsTree::Regular);
924        assert_eq!(tree["a"]["b"]["c"], FsTree::Regular);
925
926        // Paths are relative, so empty path returns the node itself
927        assert_eq!(tree[""], tree);
928        assert_eq!(tree[""], tree[""]);
929
930        // "."s are ignored
931        assert_eq!(tree["."], tree[""]);
932        assert_eq!(tree["././"], tree["."]);
933        assert_eq!(tree["././."], tree);
934        assert_eq!(tree["./a/."]["././b/./."], FsTree::from_path_text("c"));
935        assert_eq!(tree["./a/./b"]["c/."], FsTree::Regular);
936    }
937
938    // #[test]
939    // fn test_simple_merge() {
940    //     let left = FsTree::from_path_text(".config/i3/file");
941    //     let right = FsTree::from_path_text(".config/i3/folder/file");
942    //     let result = left.try_merge(right);
943
944    //     let expected = tree! {
945    //         ".config": [
946    //             i3: [
947    //                 file
948    //                 folder: [
949    //                     file
950    //                 ]
951    //             ]
952    //         ]
953    //     };
954
955    //     assert_eq!(result, Some(expected));
956    // }
957
958    #[test]
959    fn test_partial_eq_fails() {
960        let left = FsTree::from_path_text(".config/i3/a");
961        let right = FsTree::from_path_text(".config/i3/b");
962
963        assert_ne!(left, right);
964    }
965}