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}