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}