Skip to main content

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    let path = path.replace('\\', "/");
116    path.trim_start_matches('/').into()
117}
118
119/// Returns an error indicating that the path already exists.
120pub(crate) fn already_exists() -> io::Error {
121    io::Error::new(ErrorKind::AlreadyExists, "Already exists")
122}
123
124/// Returns an error indicating that the path already exists.
125pub(crate) fn invalid_input(error: &str) -> io::Error {
126    io::Error::new(ErrorKind::InvalidInput, error)
127}
128
129/// Returns an error indicating that the path already exists.
130pub(crate) fn invalid_path() -> io::Error {
131    io::Error::new(ErrorKind::InvalidInput, "Invalid path")
132}
133
134/// Returns an error indicating that the file was not found.
135pub(crate) fn not_found() -> io::Error {
136    io::Error::new(ErrorKind::NotFound, "File not found")
137}
138
139/// Returns an error indicating that the operation is not supported.
140pub(crate) fn not_supported() -> io::Error {
141    io::Error::new(ErrorKind::Unsupported, "Not supported")
142}
143
144#[cfg(test)]
145pub mod test {
146    use crate::file::Metadata;
147    use crate::util::{component_iter, create_dir_all, normalize_path, parent_iter};
148    use crate::{FileSystem, MockFileSystem};
149    use std::collections::BTreeMap;
150    use std::io;
151    use std::io::ErrorKind;
152    use std::path::Path;
153
154    /// Reads the directory and sorts all entries into a map.
155    pub(crate) fn read_directory<F: FileSystem>(fs: &F, dir: &str) -> BTreeMap<String, Metadata> {
156        fs.read_dir(dir)
157            .unwrap()
158            .map(|entry| {
159                let entry = entry.unwrap();
160                (entry.path.to_str().unwrap().to_owned(), entry.metadata)
161            })
162            .collect()
163    }
164
165    #[test]
166    fn components() {
167        itertools::assert_equal(
168            component_iter(Path::new("../many/files/and/directories/")),
169            vec!["many", "files", "and", "directories"],
170        );
171    }
172
173    const TARGET_DIR: &str = "/some/directory/somewhere/";
174
175    #[test]
176    fn create_all_happy_case() {
177        let mut mock_fs = MockFileSystem::new();
178
179        let mut i = 0;
180        mock_fs.expect_create_dir().times(3).returning(move |_| {
181            i += 1;
182
183            if i == 1 {
184                Err(io::Error::new(ErrorKind::AlreadyExists, ""))
185            } else {
186                Ok(())
187            }
188        });
189
190        assert!(create_dir_all(&mock_fs, TARGET_DIR).is_ok())
191    }
192
193    #[test]
194    fn create_all_error() {
195        let mut mock_fs = MockFileSystem::new();
196
197        mock_fs
198            .expect_create_dir()
199            .returning(|_| Err(io::Error::new(ErrorKind::Unsupported, "")));
200
201        assert!(create_dir_all(&mock_fs, TARGET_DIR).is_err())
202    }
203
204    #[test]
205    fn normalize() {
206        assert_eq!(normalize_path("///////"), Path::new("/"));
207        assert_eq!(normalize_path("./test/something/../"), Path::new("test"));
208        assert_eq!(normalize_path("../test"), Path::new("test"));
209    }
210
211    #[test]
212    fn parent() {
213        itertools::assert_equal(
214            parent_iter(Path::new("/many/files/and/directories")),
215            vec![
216                Path::new("/many/files/and"),
217                Path::new("/many/files"),
218                Path::new("/many"),
219                Path::new("/"),
220            ],
221        );
222
223        itertools::assert_equal(
224            parent_iter(Path::new("../many/files/and/directories")),
225            vec![
226                Path::new("../many/files/and"),
227                Path::new("../many/files"),
228                Path::new("../many"),
229                Path::new(".."),
230            ],
231        );
232    }
233}