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    /// On conflicts, nodes from `other` are ignored and `self` is kept unchanged.
364    ///
365    /// For conflict checking, see [`conflicts_with`], in other words, if
366    /// [`conflicts_with`] returns `true`, then at least one file will be
367    /// ignored from `other` in the `merge` call.
368    ///
369    /// [`conflicts_with`]: Self::conflicts_with
370    pub fn merge(self, other: Self) -> Self {
371        // let's merge the right (consuming) onto the left (mutating)
372        let mut left = self;
373        let right = other;
374
375        match (&mut left, right) {
376            // both a directory at the same path, try merging
377            (FsTree::Directory(left_children), FsTree::Directory(right_children)) => {
378                for (path, right_node) in right_children {
379                    // if right node exists, remove, merge and re-add, otherwise, just add it
380                    if let Some(left_node) = left_children.remove(&path) {
381                        let new_node = left_node.merge(right_node);
382                        left_children.insert(path, new_node);
383                    } else {
384                        left_children.insert(path, right_node);
385                    }
386                }
387            },
388            (_, _) => { /* conflict, but nothing to do, don't mutate left side */ },
389        }
390
391        left
392    }
393
394    /// Checks for conflicts in case the two trees would be merged.
395    ///
396    /// Rules to what a conflict is:
397    ///
398    /// - Two files have the same path, but different type.
399    /// - Two symlinks at the same path point at a different target.
400    ///
401    /// Note: directories with different children isn't considered a conflict.
402    ///
403    /// Also see [`Self::merge`] docs.
404    pub fn conflicts_with(&self, other: &Self) -> bool {
405        match (self, other) {
406            (FsTree::Directory(self_children), FsTree::Directory(other_children)) => {
407                for (path, other_node) in other_children {
408                    if let Some(self_node) = self_children.get(path.as_path())
409                        && self_node.conflicts_with(other_node)
410                    {
411                        return true;
412                    }
413                }
414            },
415            (_, _) => return true,
416        }
417
418        false
419    }
420
421    /// Reference to children if `self.is_directory()`.
422    pub fn children(&self) -> Option<&TrieMap> {
423        match &self {
424            Self::Directory(children) => Some(children),
425            _ => None,
426        }
427    }
428
429    /// Mutable reference to children if `self.is_directory()`.
430    pub fn children_mut(&mut self) -> Option<&mut TrieMap> {
431        match self {
432            Self::Directory(children) => Some(children),
433            _ => None,
434        }
435    }
436
437    /// Reference to target path, if `self.is_symlink()`.
438    pub fn target(&self) -> Option<&Path> {
439        match &self {
440            Self::Symlink(target_path) => Some(target_path),
441            _ => None,
442        }
443    }
444
445    /// Mutable reference to target path, if `self.is_symlink()`.
446    pub fn target_mut(&mut self) -> Option<&mut PathBuf> {
447        match self {
448            Self::Symlink(target_path) => Some(target_path),
449            _ => None,
450        }
451    }
452
453    // /// Apply a closure for each direct child of this FsTree.
454    // ///
455    // /// Only 1 level deep.
456    // pub fn apply_to_children0(&mut self, f: impl FnMut(&mut Self)) {
457    //     if let Some(children) = self.children_mut() {
458    //         children.iter_mut().for_each(f);
459    //     }
460    // }
461
462    // /// Apply a closure to all direct and indirect descendants inside of this structure.
463    // ///
464    // /// Calls recursively for all levels.
465    // pub fn apply_to_all_children1(&mut self, f: impl FnMut(&mut Self) + Copy) {
466    //     if let Some(children) = self.children_mut() {
467    //         children
468    //             .iter_mut()
469    //             .for_each(|x| x.apply_to_all_children1(f));
470    //         children.iter_mut().for_each(f);
471    //     }
472    // }
473
474    // /// Apply a closure to all direct and indirect descendants inside (including root).
475    // ///
476    // /// Calls recursively for all levels.
477    // pub fn apply_to_all(&mut self, mut f: impl FnMut(&mut Self) + Copy) {
478    //     f(self);
479    //     if let Some(children) = self.children_mut() {
480    //         for child in children.iter_mut() {
481    //             child.apply_to_all(f);
482    //         }
483    //     }
484    // }
485
486    /// Returns `true` if `self` is a leaf node.
487    ///
488    /// A leaf node might be of any type, including directory, however, a
489    /// non-leaf node is always a directory.
490    pub fn is_leaf(&self) -> bool {
491        match self {
492            Self::Regular | Self::Symlink(_) => true,
493            Self::Directory(children) => children.is_empty(),
494        }
495    }
496
497    /// The variant string, useful for showing to user.
498    pub fn variant_str(&self) -> &'static str {
499        match self {
500            Self::Regular => "regular file",
501            Self::Directory(_) => "directory",
502            Self::Symlink(_) => "symlink",
503        }
504    }
505
506    /// Returns `true` if self matches the [`FsTree::Regular`] variant.
507    pub fn is_regular(&self) -> bool {
508        matches!(self, Self::Regular)
509    }
510
511    /// Returns `true` if self matches the [`FsTree::Directory`] variant.
512    pub fn is_dir(&self) -> bool {
513        matches!(self, Self::Directory(_))
514    }
515
516    /// Returns `true` if self matches the [`FsTree::Symlink`] variant.
517    pub fn is_symlink(&self) -> bool {
518        matches!(self, Self::Symlink(_))
519    }
520
521    // /// Generate a diff from two different trees.
522    // pub fn diff(&self, other: &Self) {
523    //     if !self.has_same_type_as(other) {
524    //         println!("Types differ! ");
525    //     }
526
527    //     let (self_children, other_children) = match (&self.file_type, &other.file_type) {
528    //         (Self::Directory(self_children), Self::Directory(other_children)) => {
529    //             (self_children, other_children)
530    //         },
531    //         _ => panic!(),
532    //     };
533
534    //     let mut lookup = self_children
535    //         .iter()
536    //         .map(|x| (&x.path, x))
537    //         .collect::<HashMap<&PathBuf, &FsTree>>();
538
539    //     for other_child in other_children {
540    //         if let Some(self_child) = lookup.remove(&other_child.path) {
541    //             if self_child.has_same_type_as(other_child) {
542    //                 if self_child.is_dir() {
543    //                     self_child.diff(other_child);
544    //                 }
545    //             } else {
546    //                 println!(
547    //                     "File {:?} is a {} while file {:?} is a {}",
548    //                     self_child.path,
549    //                     self_child.file_type.file_type_display(),
550    //                     other_child.path,
551    //                     other_child.file_type.file_type_display(),
552    //                 );
553    //             }
554    //         } else {
555    //             let path = &other_child.path;
556    //             println!(
557    //                 "2Only in {:?}: {:?}",
558    //                 path.parent().unwrap(),
559    //                 path.file_name().unwrap()
560    //             );
561    //         }
562    //     }
563
564    //     for child_left in lookup.values() {
565    //         let path = &child_left.path;
566    //         println!(
567    //             "1Only in {:?}: {:?}",
568    //             path.parent().unwrap(),
569    //             path.file_name().unwrap()
570    //         );
571    //     }
572    // }
573
574    /// Write the tree structure at the given folder path.
575    ///
576    /// This method creates files, directories, and symlinks as defined by the
577    /// tree structure. When a path already exists, the behavior depends on the
578    /// node type:
579    ///
580    /// - **Regular files**: If a regular file already exists at the path, it is
581    ///   left unchanged. If a non-regular file exists, returns
582    ///   [`Error::NotARegularFile`].
583    ///
584    /// - **Directories**: If a directory already exists at the path, it is left
585    ///   unchanged. If a non-directory exists, returns [`Error::NotADirectory`].
586    ///
587    /// - **Symlinks**: If a symlink already exists and points to the expected
588    ///   target, it is left unchanged. If a symlink exists but points to a
589    ///   different target, returns [`Error::SymlinkTargetMismatch`]. If a
590    ///   non-symlink exists, returns [`Error::NotASymlink`].
591    ///
592    /// # Errors
593    ///
594    /// - If the provided folder doesn't exist, or is not a directory.
595    /// - If a path conflict occurs (see above for conflict rules).
596    /// - If any other IO error occurs.
597    ///
598    /// [`Error::NotARegularFile`]: crate::Error::NotARegularFile
599    /// [`Error::NotADirectory`]: crate::Error::NotADirectory
600    /// [`Error::NotASymlink`]: crate::Error::NotASymlink
601    /// [`Error::SymlinkTargetMismatch`]: crate::Error::SymlinkTargetMismatch
602    pub fn write_structure_at(&self, folder: impl AsRef<Path>) -> Result<()> {
603        let folder = folder.as_ref();
604
605        #[cfg(feature = "fs-err")]
606        let symlink_function = fs_err::os::unix::fs::symlink;
607        #[cfg(not(feature = "fs-err"))]
608        let symlink_function = std::os::unix::fs::symlink;
609
610        for (node, relative_path) in self.iter().skip(1) {
611            let path = folder.join(&relative_path);
612
613            match &node {
614                Self::Regular => {
615                    if path.exists() {
616                        if !path.is_file() {
617                            return Err(Error::NotARegularFile(path));
618                        }
619                    } else {
620                        fs::File::create(path)?;
621                    }
622                },
623                Self::Directory(_) => {
624                    if path.exists() {
625                        if !path.is_dir() {
626                            return Err(Error::NotADirectory(path));
627                        }
628                    } else {
629                        fs::create_dir(path)?;
630                    }
631                },
632                Self::Symlink(expected_target) => {
633                    match FileType::symlink_read_at(&path) {
634                        Ok(file_type) if file_type.is_symlink() => {
635                            let actual_target = fs::read_link(&path)?;
636                            if actual_target != *expected_target {
637                                return Err(Error::SymlinkTargetMismatch {
638                                    path,
639                                    expected: expected_target.clone(),
640                                    found: actual_target,
641                                });
642                            }
643                        },
644                        Ok(_) => {
645                            return Err(Error::NotASymlink(path));
646                        },
647                        Err(_) => {
648                            symlink_function(expected_target, path)?;
649                        },
650                    }
651                },
652            }
653        }
654
655        Ok(())
656    }
657
658    /// Returns a reference to the node at the path, if any.
659    ///
660    /// # Errors:
661    ///
662    /// - Returns `None` if there is no node at the given path.
663    ///
664    /// # Examples:
665    ///
666    /// ```
667    /// use fs_tree::FsTree;
668    ///
669    /// let root = FsTree::from_path_text("a/b/c");
670    ///
671    /// // Indexing is relative from `root`, so `root` cannot be indexed.
672    /// assert_eq!(root, FsTree::from_path_text("a/b/c"));
673    /// assert_eq!(root["a"], FsTree::from_path_text("b/c"));
674    /// assert_eq!(root["a/b"], FsTree::from_path_text("c"));
675    /// assert_eq!(root["a"]["b"], FsTree::from_path_text("c"));
676    /// assert_eq!(root["a/b/c"], FsTree::Regular);
677    /// assert_eq!(root["a/b"]["c"], FsTree::Regular);
678    /// assert_eq!(root["a"]["b/c"], FsTree::Regular);
679    /// assert_eq!(root["a"]["b"]["c"], FsTree::Regular);
680    /// ```
681    pub fn get(&self, path: impl AsRef<Path>) -> Option<&Self> {
682        let path = path.as_ref();
683
684        // Split first piece from the rest
685        let (popped, path_rest) = {
686            let mut iter = path.iter();
687            let popped: Option<&Path> = iter.next().map(OsStr::as_ref);
688            (popped, iter.as_path())
689        };
690
691        // If path ended, we reached the desired node
692        let Some(popped) = popped else {
693            return Some(self);
694        };
695
696        // Corner case: if `.`, ignore it and call again with the rest
697        if popped == Path::new(".") {
698            return self.get(path_rest);
699        }
700
701        self.children()?
702            .get(popped)
703            .and_then(|child| child.get(path_rest))
704    }
705
706    /// Returns a mutable reference to the node at the path, if any.
707    ///
708    /// This is the mutable version of [`FsTree::get`].
709    pub fn get_mut(&mut self, path: impl AsRef<Path>) -> Option<&mut Self> {
710        let path = path.as_ref();
711
712        // Split first piece from the rest
713        let (popped, path_rest) = {
714            let mut iter = path.iter();
715            let popped: Option<&Path> = iter.next().map(OsStr::as_ref);
716            (popped, iter.as_path())
717        };
718
719        // If path ended, we reached the desired node
720        let Some(popped) = popped else {
721            return Some(self);
722        };
723
724        // Corner case: if `.`, ignore it and call again with the rest
725        if popped == Path::new(".") {
726            return self.get_mut(path_rest);
727        }
728
729        self.children_mut()?
730            .get_mut(popped)
731            .and_then(|child| child.get_mut(path_rest))
732    }
733
734    /// Inserts a node at the given path.
735    ///
736    /// # Panics:
737    ///
738    /// - If there are no directories up to the path node in order to insert it.
739    /// - If path is empty.
740    pub fn insert(&mut self, path: impl AsRef<Path>, node: Self) {
741        use FsTree::*;
742
743        let mut iter = path.as_ref().iter();
744
745        let Some(node_name) = iter.next_back().map(Path::new) else {
746            *self = node;
747            return;
748        };
749
750        let mut tree = self;
751
752        // Traverse tree
753        for next in iter {
754            // Give a better error message than the one below
755            if !tree.is_dir() {
756                panic!(
757                    "Failed to insert node, while traversing, one of the parent directories \
758                    ({next:?}) isn't a directory, but a {}",
759                    tree.variant_str()
760                );
761            }
762
763            tree = if let Some(tree) = tree.get_mut(next) {
764                tree
765            } else {
766                panic!("Failed to insert node, parent directory {next:?} doesn't exist");
767            };
768        }
769
770        match tree {
771            Regular | Symlink(_) => {
772                panic!(
773                    "Failed to insert node, parent directory is not a directory, but a {}",
774                    tree.variant_str(),
775                );
776            },
777            Directory(children) => {
778                children.insert(node_name.into(), node);
779            },
780        }
781    }
782}
783
784#[cfg(feature = "libc-file-type")]
785impl FsTree {
786    /// Returns the file type equivalent [`libc::mode_t`] value.
787    pub fn as_mode_t(&self) -> libc::mode_t {
788        match self {
789            Self::Regular => libc::S_IFREG,
790            Self::Directory(_) => libc::S_IFDIR,
791            Self::Symlink(_) => libc::S_IFCHR,
792        }
793    }
794}
795
796impl<P> Index<P> for FsTree
797where
798    P: AsRef<Path>,
799{
800    type Output = FsTree;
801
802    fn index(&self, path: P) -> &Self::Output {
803        self.get(path.as_ref())
804            .unwrap_or_else(|| panic!("no node found for path '{}'", path.as_ref().display()))
805    }
806}
807
808#[cfg(test)]
809mod tests {
810    use std::{io, path::Path};
811
812    use pretty_assertions::{assert_eq, assert_ne};
813
814    use super::*;
815    use crate::tree;
816
817    fn testdir() -> io::Result<(tempfile::TempDir, &'static Path)> {
818        let dir = tempfile::tempdir()?;
819        let path = dir.path().to_path_buf().into_boxed_path();
820        Ok((dir, Box::leak(path)))
821    }
822
823    #[test]
824    fn test_len_all_counts_all_nodes_including_root() {
825        let tree = tree! {
826            file1
827            dir1: [
828                file2
829                file3
830            ]
831            file4
832        };
833
834        assert_eq!(tree.len_all(), 6);
835    }
836
837    // #[test]
838    // fn test_diff() {
839    //     let left = FsTree::from_path_text(".config/i3/file").unwrap();
840    //     let right = FsTree::from_path_text(".config/i3/folder/file/oie").unwrap();
841    //     left.diff(&right);
842    //     panic!();
843    // }
844
845    #[test]
846    fn test_insert_basic() {
847        let mut tree = FsTree::new_dir();
848
849        let paths = ["a", "a/b", "a/b/c", "a/b/c/d", "a/b/c/d/e"];
850        for path in paths {
851            tree.insert(path, FsTree::new_dir());
852        }
853
854        tree.insert("a/b/c/d/e/f", FsTree::Regular);
855
856        let expected = tree! {
857            a: [ b: [ c: [ d: [ e: [ f ] ] ] ] ]
858        };
859
860        assert_eq!(tree, expected);
861    }
862
863    #[rustfmt::skip]
864    #[test]
865    fn test_insert_complete() {
866        let result = {
867            let mut tree = FsTree::new_dir();
868            tree.insert("config1", FsTree::Regular);
869            tree.insert("config2", FsTree::Regular);
870            tree.insert("outer_dir", FsTree::new_dir());
871            tree.insert("outer_dir/file1", FsTree::Regular);
872            tree.insert("outer_dir/file2", FsTree::Regular);
873            tree.insert("outer_dir/inner_dir", FsTree::new_dir());
874            tree.insert("outer_dir/inner_dir/inner1", FsTree::Regular);
875            tree.insert("outer_dir/inner_dir/inner2", FsTree::Regular);
876            tree.insert("outer_dir/inner_dir/inner3", FsTree::Regular);
877            tree.insert("outer_dir/inner_dir/inner_link", FsTree::Symlink("inner_target".into()));
878            tree.insert("link", FsTree::Symlink("target".into()));
879            tree.insert("config3", FsTree::Regular);
880            tree
881        };
882
883        let expected = tree! {
884            config1
885            config2
886            outer_dir: [
887                file1
888                file2
889                inner_dir: [
890                    inner1
891                    inner2
892                    inner3
893                    inner_link -> inner_target
894                ]
895            ]
896            link -> target
897            config3
898        };
899
900        assert_eq!(result, expected);
901    }
902
903    #[test]
904    fn test_write_structure_at() {
905        let (_dropper, test_dir) = testdir().unwrap();
906
907        let tree = tree! {
908            a: [
909                b: [
910                    c
911                    empty: []
912                    link -> target
913                ]
914            ]
915        };
916
917        tree.write_structure_at(test_dir).unwrap();
918
919        let result = FsTree::symlink_read_at(test_dir).unwrap();
920
921        assert_eq!(result, tree);
922    }
923
924    #[test]
925    fn test_get() {
926        let tree = FsTree::from_path_text("a/b/c");
927
928        assert_eq!(tree["a"], FsTree::from_path_text("b/c"));
929        assert_eq!(tree["a/b"], FsTree::from_path_text("c"));
930        assert_eq!(tree["a"]["b"], FsTree::from_path_text("c"));
931        assert_eq!(tree["a/b/c"], FsTree::Regular);
932        assert_eq!(tree["a/b"]["c"], FsTree::Regular);
933        assert_eq!(tree["a"]["b/c"], FsTree::Regular);
934        assert_eq!(tree["a"]["b"]["c"], FsTree::Regular);
935
936        // Paths are relative, so empty path returns the node itself
937        assert_eq!(tree[""], tree);
938        assert_eq!(tree[""], tree[""]);
939
940        // "."s are ignored
941        assert_eq!(tree["."], tree[""]);
942        assert_eq!(tree["././"], tree["."]);
943        assert_eq!(tree["././."], tree);
944        assert_eq!(tree["./a/."]["././b/./."], FsTree::from_path_text("c"));
945        assert_eq!(tree["./a/./b"]["c/."], FsTree::Regular);
946    }
947
948    #[test]
949    fn test_merge_two_paths_with_partial_intersection() {
950        let left = FsTree::from_path_text(".config/i3/file");
951        let right = FsTree::from_path_text(".config/i3/folder/file");
952        assert!(!left.conflicts_with(&right));
953        let result = left.merge(right);
954        assert_eq!(
955            result,
956            tree! {
957                ".config": [
958                    i3: [
959                        file
960                        folder: [
961                            file
962                        ]
963                    ]
964                ]
965            }
966        );
967    }
968
969    #[test]
970    fn test_partial_eq_fails() {
971        let left = FsTree::from_path_text(".config/i3/a");
972        let right = FsTree::from_path_text(".config/i3/b");
973
974        assert_ne!(left, right);
975    }
976
977    #[test]
978    fn test_merge_disjoint_trees() {
979        let left = tree! { a };
980        let right = tree! { b };
981        assert!(!left.conflicts_with(&right));
982        let merged = left.merge(right);
983        assert_eq!(
984            merged,
985            tree! {
986                a
987                b
988            }
989        );
990    }
991
992    #[test]
993    fn test_merge_conflict_keeps_self() {
994        let left = tree! {
995            a
996            b -> c
997            link -> v1
998        };
999        let right = tree! {
1000            a: []
1001            b
1002            link -> v2
1003        };
1004        assert!(left.conflicts_with(&right));
1005        let merged = left.clone().merge(right);
1006        assert_eq!(merged, left);
1007    }
1008
1009    #[test]
1010    fn test_merge_nested_directories() {
1011        let left = tree! { dir: [a] };
1012        let right = tree! { dir: [b] };
1013        assert!(!left.conflicts_with(&right));
1014        let merged = left.merge(right);
1015        assert_eq!(
1016            merged,
1017            tree! {
1018                dir: [
1019                    a
1020                    b
1021                ]
1022            }
1023        );
1024    }
1025
1026    #[test]
1027    fn test_conflicts_with() {
1028        let left = tree! { a };
1029        let right = tree! { b };
1030        assert!(!left.conflicts_with(&right));
1031
1032        let left = tree! { file };
1033        let right = tree! { file };
1034        assert!(left.conflicts_with(&right));
1035
1036        let left = tree! { x };
1037        let right = tree! { x: [] };
1038        assert!(left.conflicts_with(&right));
1039
1040        let left = tree! { a -> b };
1041        let right = tree! { a };
1042        assert!(left.conflicts_with(&right));
1043
1044        let left = tree! { a -> b };
1045        let right = tree! { a: [] };
1046        assert!(left.conflicts_with(&right));
1047
1048        let left = tree! { a -> b };
1049        let right = tree! { a -> c };
1050        assert!(left.conflicts_with(&right));
1051
1052        let left = tree! { a -> c };
1053        let right = tree! { b -> c };
1054        assert!(!left.conflicts_with(&right));
1055    }
1056}