virtual_filesystem/
util.rs

1use crate::FileSystem;
2use normalize_path::NormalizePath;
3use path_slash::PathBufExt;
4use std::io;
5use std::io::ErrorKind;
6use std::iter::once;
7use std::path::{Component, Path, PathBuf};
8
9/// Iterates over all path components.
10///
11/// # Arguments
12/// `path`: The current path.  
13///
14/// # Example
15/// ```
16/// use std::path::Path;
17/// use virtual_filesystem::util::component_iter;
18///
19/// itertools::assert_equal(
20///     component_iter(Path::new("../many/files/and/directories/")),
21///     vec!["many", "files", "and", "directories"],
22/// );
23/// ```
24pub fn component_iter(path: &Path) -> impl DoubleEndedIterator<Item = &str> {
25    path.components().filter_map(|component| {
26        if let Component::Normal(component) = component {
27            component.to_str()
28        } else {
29            None
30        }
31    })
32}
33
34/// Creates all directories by iteratively creating parent directories. Returns an error if the operation fails for
35/// any reason other than `AlreadyExists`.
36///
37/// # Arguments
38/// `fs`: The filesystem.  
39/// `path`: The path of the directory to create.  
40pub fn create_dir_all<FS: FileSystem + ?Sized>(fs: &FS, path: &str) -> crate::Result<()> {
41    let normalized = normalize_path(make_relative(path));
42
43    for path in parent_iter(&normalized).chain(once(normalized.as_ref())) {
44        // unwrap: `path` should already be a valid UTF-8 string
45        if let Err(err) = fs.create_dir(path.to_str().unwrap()) {
46            if err.kind() != ErrorKind::AlreadyExists {
47                return Err(err);
48            }
49        }
50    }
51
52    Ok(())
53}
54
55/// Normalizes a path by stripping slashes, resolving backtracking, and using forward slashes.
56///
57/// # Arguments
58/// `path`: The path to normalize.  
59///
60/// # Example
61/// ```
62/// use std::path::Path;
63/// use virtual_filesystem::util::normalize_path;
64///
65/// assert_eq!(normalize_path("///////"), Path::new("/"));
66/// assert_eq!(normalize_path("./test/something/../"), Path::new("test"));
67/// assert_eq!(normalize_path("../test"), Path::new("test"));
68/// ```
69pub fn normalize_path<P: AsRef<Path>>(path: P) -> PathBuf {
70    Path::new(path.as_ref().normalize().to_slash_lossy().as_ref()).to_owned()
71}
72
73/// Produces an iterator iterating over all parent directories, exclusive of `path`.
74///
75/// # Arguments
76/// `path`: The current path.  
77///
78/// # Example
79/// ```
80/// use std::path::Path;
81/// use virtual_filesystem::util::parent_iter;
82///
83/// itertools::assert_equal(
84///     parent_iter(Path::new("/many/files/and/directories")),
85///     vec![
86///         Path::new("/many/files/and"),
87///         Path::new("/many/files"),
88///         Path::new("/many"),
89///         Path::new("/"),
90///     ],
91/// );
92///
93/// itertools::assert_equal(
94///     parent_iter(Path::new("../many/files/and/directories")),
95///     vec![
96///         Path::new("../many/files/and"),
97///         Path::new("../many/files"),
98///         Path::new("../many"),
99///         Path::new(".."),
100///     ],
101/// );
102/// ```
103pub fn parent_iter(path: &Path) -> impl DoubleEndedIterator<Item = &Path> {
104    // collect parent paths
105    path.ancestors()
106        .filter(|path| !path.as_os_str().is_empty())
107        .skip(1)
108        .collect::<Vec<_>>()
109        .into_iter()
110}
111
112/// Trims the `/` and `\\` roots off of the beginning path, making it relative.
113pub(crate) fn make_relative<P: AsRef<Path>>(path: P) -> PathBuf {
114    let path = path.as_ref().to_str().unwrap_or("");
115    path.trim_start_matches('/').trim_start_matches('\\').into()
116}
117
118/// Returns an error indicating that the path already exists.
119pub(crate) fn already_exists() -> io::Error {
120    io::Error::new(ErrorKind::AlreadyExists, "Already exists")
121}
122
123/// Returns an error indicating that the path already exists.
124pub(crate) fn invalid_input(error: &str) -> io::Error {
125    io::Error::new(ErrorKind::InvalidInput, error)
126}
127
128/// Returns an error indicating that the path already exists.
129pub(crate) fn invalid_path() -> io::Error {
130    io::Error::new(ErrorKind::InvalidInput, "Invalid path")
131}
132
133/// Returns an error indicating that the file was not found.
134pub(crate) fn not_found() -> io::Error {
135    io::Error::new(ErrorKind::NotFound, "File not found")
136}
137
138/// Returns an error indicating that the operation is not supported.
139pub(crate) fn not_supported() -> io::Error {
140    io::Error::new(ErrorKind::Unsupported, "Not supported")
141}
142
143#[cfg(test)]
144pub mod test {
145    use crate::file::Metadata;
146    use crate::util::{component_iter, create_dir_all, normalize_path, parent_iter};
147    use crate::{FileSystem, MockFileSystem};
148    use std::collections::BTreeMap;
149    use std::io;
150    use std::io::ErrorKind;
151    use std::path::Path;
152
153    /// Reads the directory and sorts all entries into a map.
154    pub(crate) fn read_directory<F: FileSystem>(fs: &F, dir: &str) -> BTreeMap<String, Metadata> {
155        fs.read_dir(dir)
156            .unwrap()
157            .map(|entry| {
158                let entry = entry.unwrap();
159                (entry.path.to_str().unwrap().to_owned(), entry.metadata)
160            })
161            .collect()
162    }
163
164    #[test]
165    fn components() {
166        itertools::assert_equal(
167            component_iter(Path::new("../many/files/and/directories/")),
168            vec!["many", "files", "and", "directories"],
169        );
170    }
171
172    const TARGET_DIR: &str = "/some/directory/somewhere/";
173
174    #[test]
175    fn create_all_happy_case() {
176        let mut mock_fs = MockFileSystem::new();
177
178        let mut i = 0;
179        mock_fs.expect_create_dir().times(3).returning(move |_| {
180            i += 1;
181
182            if i == 1 {
183                Err(io::Error::new(ErrorKind::AlreadyExists, ""))
184            } else {
185                Ok(())
186            }
187        });
188
189        assert!(create_dir_all(&mock_fs, TARGET_DIR).is_ok())
190    }
191
192    #[test]
193    fn create_all_error() {
194        let mut mock_fs = MockFileSystem::new();
195
196        mock_fs
197            .expect_create_dir()
198            .returning(|_| Err(io::Error::new(ErrorKind::Unsupported, "")));
199
200        assert!(create_dir_all(&mock_fs, TARGET_DIR).is_err())
201    }
202
203    #[test]
204    fn normalize() {
205        assert_eq!(normalize_path("///////"), Path::new("/"));
206        assert_eq!(normalize_path("./test/something/../"), Path::new("test"));
207        assert_eq!(normalize_path("../test"), Path::new("test"));
208    }
209
210    #[test]
211    fn parent() {
212        itertools::assert_equal(
213            parent_iter(Path::new("/many/files/and/directories")),
214            vec![
215                Path::new("/many/files/and"),
216                Path::new("/many/files"),
217                Path::new("/many"),
218                Path::new("/"),
219            ],
220        );
221
222        itertools::assert_equal(
223            parent_iter(Path::new("../many/files/and/directories")),
224            vec![
225                Path::new("../many/files/and"),
226                Path::new("../many/files"),
227                Path::new("../many"),
228                Path::new(".."),
229            ],
230        );
231    }
232}