broot/tree/
tree_line_type.rs

1use {
2    rustc_hash::FxHashSet,
3    std::{
4        fs,
5        io,
6        path::{
7            Path,
8            PathBuf,
9        },
10    },
11};
12
13/// The maximum number of symlink hops before giving up.
14const MAX_LINK_CHAIN_LENGTH: usize = 128;
15
16/// The type of a line which can be displayed as
17/// part of a tree
18#[derive(Debug, Clone, PartialEq)]
19pub enum TreeLineType {
20    File,
21    Dir,
22    BrokenSymLink(String),
23    SymLink {
24        direct_target: String,
25        final_is_dir: bool,
26        final_target: PathBuf,
27    },
28    Pruning, // a "xxx unlisted" line
29}
30
31pub fn read_link(path: &Path) -> io::Result<PathBuf> {
32    let mut target = fs::read_link(path)?;
33    if target.is_relative() {
34        target = path.parent().unwrap().join(&target);
35    }
36    Ok(target)
37}
38
39impl TreeLineType {
40    pub fn is_pruning(&self) -> bool {
41        matches!(self, Self::Pruning)
42    }
43
44    fn resolve(direct_target: &Path) -> io::Result<Self> {
45        let mut final_target = direct_target.to_path_buf();
46        let mut final_metadata = fs::symlink_metadata(&final_target)?;
47        let mut final_ft = final_metadata.file_type();
48        let mut final_is_dir = final_ft.is_dir();
49        let mut link_chain_length = 0;
50        let mut visited = FxHashSet::default();
51        while final_ft.is_symlink() {
52            final_target = read_link(&final_target)?;
53            if visited.contains(&final_target) {
54                info!(
55                    "circular symlink opened by {} and closed by {}",
56                    direct_target.display(),
57                    final_target.display(),
58                );
59                return Ok(Self::BrokenSymLink(
60                    direct_target.to_string_lossy().into_owned(),
61                ));
62            }
63            visited.insert(final_target.clone());
64            final_metadata = fs::symlink_metadata(&final_target)?;
65            final_ft = final_metadata.file_type();
66            final_is_dir = final_ft.is_dir();
67            link_chain_length += 1;
68            if link_chain_length > MAX_LINK_CHAIN_LENGTH {
69                info!("too long link chain at {}", direct_target.display());
70                return Ok(Self::BrokenSymLink(
71                    direct_target.to_string_lossy().into_owned(),
72                ));
73            }
74        }
75        let direct_target = direct_target.to_string_lossy().into_owned();
76        Ok(Self::SymLink {
77            direct_target,
78            final_is_dir,
79            final_target,
80        })
81    }
82
83    pub fn new(
84        path: &Path,
85        ft: fs::FileType,
86    ) -> Self {
87        if ft.is_dir() {
88            Self::Dir
89        } else if ft.is_symlink() {
90            if let Ok(direct_target) = read_link(path) {
91                Self::resolve(&direct_target).unwrap_or_else(|_| {
92                    Self::BrokenSymLink(direct_target.to_string_lossy().to_string())
93                })
94            } else {
95                Self::BrokenSymLink("???".to_string())
96            }
97        } else {
98            Self::File
99        }
100    }
101}