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