champ/
path.rs

1use crate::{fail, util};
2use std::{ffi, fmt, path};
3
4#[derive(Clone, PartialEq, PartialOrd, Ord, Eq, Debug)]
5pub enum Part {
6    Folder { name: ffi::OsString },
7    File { name: ffi::OsString },
8    Range { begin: usize, size: usize },
9}
10
11#[derive(Clone, PartialOrd, PartialEq, Ord, Eq, Debug)]
12pub struct Path {
13    parts: Vec<Part>,
14}
15
16impl Path {
17    pub fn root() -> Path {
18        Path { parts: Vec::new() }
19    }
20    pub fn folder(path: impl AsRef<path::Path>) -> Path {
21        let mut p = Path::root();
22        for component in path.as_ref().components() {
23            let component = component.as_os_str();
24            if component != "/" {
25                p = p.push_clone(Part::Folder {
26                    name: component.into(),
27                });
28            }
29        }
30        p
31    }
32    pub fn file(path: impl AsRef<path::Path>) -> Path {
33        let mut p = Path::folder(path);
34        // Replace the last part into Part::File
35        if let Some(part) = p.parts.pop() {
36            if let Part::Folder { name } = part {
37                p.parts.push(Part::File { name });
38            }
39        }
40        p
41    }
42    pub fn current() -> util::Result<Path> {
43        Ok(Path::folder(std::env::current_dir()?))
44    }
45
46    pub fn include(&self, rhs: &Path) -> bool {
47        let parts_to_check = rhs.parts.len();
48        if self.parts.len() < parts_to_check {
49            return false;
50        }
51        for ix in 0..parts_to_check {
52            if rhs.parts[ix] != self.parts[ix] {
53                return false;
54            }
55        }
56        true
57    }
58    pub fn is_hidden(&self) -> bool {
59        let is_hidden = |name: &ffi::OsString| {
60            if let Some(ch) = name.to_string_lossy().chars().next() {
61                if ch == '.' {
62                    return true;
63                }
64            }
65            false
66        };
67        for part in &self.parts {
68            match part {
69                Part::Folder { name } => {
70                    if is_hidden(name) {
71                        return true;
72                    }
73                }
74                Part::File { name } => {
75                    if is_hidden(name) {
76                        return true;
77                    }
78                }
79                _ => {}
80            }
81        }
82        false
83    }
84    pub fn is_empty(&self) -> bool {
85        self.parts.is_empty()
86    }
87    pub fn is_folder(&self) -> bool {
88        for part in &self.parts {
89            match part {
90                Part::Folder { .. } => {}
91                Part::File { .. } => return false,
92                _ => {}
93            }
94        }
95        true
96    }
97    pub fn is_file(&self) -> bool {
98        !self.is_folder()
99    }
100    pub fn extension(&self) -> Option<&ffi::OsStr> {
101        for part in &self.parts {
102            match part {
103                Part::Folder { .. } => {}
104                Part::File { name } => return path::Path::new(name).extension(),
105                _ => {}
106            }
107        }
108        None
109    }
110    pub fn push(&mut self, part: Part) {
111        self.parts.push(part);
112    }
113    pub fn pop(&mut self) -> Option<Part> {
114        self.parts.pop()
115    }
116    pub fn push_clone(&self, part: Part) -> Path {
117        let mut path = self.clone();
118        path.push(part);
119        path
120    }
121    pub fn fs_path(&self) -> util::Result<FsPath> {
122        let mut ret = FsPath::Folder(path::PathBuf::from("/"));
123        for part in &self.parts {
124            match part {
125                Part::Folder { name } => match ret {
126                    FsPath::Folder(mut folder) => {
127                        folder.push(name);
128                        ret = FsPath::Folder(folder)
129                    }
130                    _ => fail!("Cannot add folder part to non-folder"),
131                },
132                Part::File { name } => match ret {
133                    FsPath::Folder(mut folder) => {
134                        folder.push(name);
135                        ret = FsPath::File(folder)
136                    }
137                    _ => fail!("Cannot add file part to non-folder"),
138                },
139                Part::Range { .. } => match &ret {
140                    FsPath::File(_) => {}
141                    _ => fail!("Cannot add range part to non-file"),
142                },
143            }
144        }
145        Ok(ret)
146    }
147    pub fn path_buf(&self) -> std::path::PathBuf {
148        let mut res = std::path::PathBuf::new();
149        res.push("/");
150        for part in &self.parts {
151            match part {
152                Part::Folder { name } => res.push(name),
153                Part::File { name } => res.push(name),
154                _ => {}
155            }
156        }
157        res
158    }
159    pub fn relative_from(&self, base: &Path) -> std::path::PathBuf {
160        let mut start_ix = Some(0);
161        for (ix, part) in base.parts.iter().enumerate() {
162            if ix >= self.parts.len() || &self.parts[ix] != part {
163                start_ix = None;
164                break;
165            }
166            start_ix = Some(ix);
167        }
168        if let Some(start_ix) = start_ix {
169            let mut rel = std::path::PathBuf::new();
170            for ix in start_ix + 1..self.parts.len() {
171                if ix < self.parts.len() {
172                    let part = &self.parts[ix];
173                    match part {
174                        Part::Folder { name } => rel.push(name),
175                        Part::File { name } => rel.push(name),
176                        Part::Range { .. } => break,
177                    }
178                }
179            }
180            rel
181        } else {
182            self.path_buf()
183        }
184    }
185    pub fn exist(&self) -> bool {
186        if let Ok(fs_path) = self.fs_path() {
187            let path;
188            match fs_path {
189                FsPath::File(p) => path = p,
190                FsPath::Folder(p) => path = p,
191            }
192            return std::fs::metadata(path).is_ok();
193        }
194        false
195    }
196    pub fn keep_folder(&mut self) {
197        while let Some(part) = self.parts.last() {
198            match part {
199                Part::Folder { .. } => {
200                    break;
201                }
202                _ => {}
203            }
204        }
205    }
206}
207
208impl fmt::Display for Path {
209    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
210        write!(f, "/")?;
211        for part in &self.parts {
212            match part {
213                Part::Folder { name } => write!(f, "{}/", name.to_string_lossy())?,
214                Part::File { name } => write!(f, "{} ", name.to_string_lossy())?,
215                Part::Range { begin, size } => write!(f, "[{}, {}]", begin, size)?,
216            }
217        }
218        Ok(())
219    }
220}
221
222#[derive(Debug)]
223pub enum FsPath {
224    Folder(path::PathBuf),
225    File(path::PathBuf),
226}
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231
232    #[test]
233    fn test_path_buf() {
234        let mut path = Path::root();
235        assert_eq!(path.path_buf(), std::path::PathBuf::from("/"));
236        path.push(Part::Folder {
237            name: "base".into(),
238        });
239        assert_eq!(path.path_buf(), std::path::PathBuf::from("/base"));
240    }
241
242    #[test]
243    fn test_relative() {
244        let base = Path::folder("/base");
245        let file = Path::file("/base/rel/name.txt");
246        assert_eq!(
247            file.relative_from(&base),
248            std::path::PathBuf::from("rel/name.txt")
249        );
250    }
251}