diskplan_filesystem/
lib.rs

1//! Provides an abstract [`Filesystem`] trait, together with a physical ([`DiskFilesystem`])
2//! and virtual ([`MemoryFilesystem`]) implementation.
3#![warn(missing_docs)]
4
5use std::fmt::Display;
6
7use anyhow::{bail, Result};
8use camino::{Utf8Component, Utf8Path, Utf8PathBuf};
9
10mod attributes;
11mod memory;
12mod physical;
13mod root;
14
15pub use self::{
16    attributes::{Attrs, Mode, SetAttrs, DEFAULT_DIRECTORY_MODE, DEFAULT_FILE_MODE},
17    memory::MemoryFilesystem,
18    physical::DiskFilesystem,
19    root::Root,
20};
21
22impl SetAttrs<'_> {
23    /// Returns true if this `SetAttrs` matches the given, existing `attrs`
24    pub fn matches(&self, attrs: &Attrs) -> bool {
25        let SetAttrs { owner, group, mode } = self;
26        owner.map(|owner| owner == attrs.owner).unwrap_or(true)
27            && group.map(|group| group == attrs.group).unwrap_or(true)
28            && mode.map(|mode| mode == attrs.mode).unwrap_or(true)
29    }
30}
31
32/// Operations of a file system
33pub trait Filesystem {
34    /// Create a directory at the given path, with any number of attributes set
35    fn create_directory(&mut self, path: impl AsRef<Utf8Path>, attrs: SetAttrs) -> Result<()>;
36
37    /// Create a directory and all of its parents
38    fn create_directory_all(&mut self, path: impl AsRef<Utf8Path>, attrs: SetAttrs) -> Result<()> {
39        let path = path.as_ref();
40        if let Some((parent, _)) = split(path) {
41            if parent != "/" {
42                self.create_directory_all(parent, attrs.clone())?;
43            }
44        }
45        if !self.is_directory(path) {
46            self.create_directory(path, attrs)?;
47        }
48        Ok(())
49    }
50
51    /// Create a file with the given content and any number of attributes set
52    fn create_file(
53        &mut self,
54        path: impl AsRef<Utf8Path>,
55        attrs: SetAttrs,
56        content: String,
57    ) -> Result<()>;
58
59    /// Create a symlink pointing to the given target
60    fn create_symlink(
61        &mut self,
62        path: impl AsRef<Utf8Path>,
63        target: impl AsRef<Utf8Path>,
64    ) -> Result<()>;
65
66    /// Returns true if the path exists
67    fn exists(&self, path: impl AsRef<Utf8Path>) -> bool;
68
69    /// Returns true if the path is a directory
70    fn is_directory(&self, path: impl AsRef<Utf8Path>) -> bool;
71
72    /// Returns true if the path is a regular file
73    fn is_file(&self, path: impl AsRef<Utf8Path>) -> bool;
74
75    /// Returns true if the path is a symbolic link
76    fn is_link(&self, path: impl AsRef<Utf8Path>) -> bool;
77
78    /// Lists the contents of the given directory
79    fn list_directory(&self, path: impl AsRef<Utf8Path>) -> Result<Vec<String>>;
80
81    /// Reads the contents of the given file
82    fn read_file(&self, path: impl AsRef<Utf8Path>) -> Result<String>;
83
84    /// Reads the path pointed to by the given symbolic link
85    fn read_link(&self, path: impl AsRef<Utf8Path>) -> Result<Utf8PathBuf>;
86
87    /// Returns the attributes of the given file, directory
88    ///
89    /// If the path is a symlink, the file/directory pointed to by the symlink will be checked
90    /// and its attributes returned (i.e. paths are dereferenced)
91    fn attributes(&self, path: impl AsRef<Utf8Path>) -> Result<Attrs>;
92
93    /// Sets the attributes of the given file or directory
94    ///
95    /// If the path is a symlink, the file/directory pointed to by the symlink will be updated
96    /// with the given attributes (i.e. paths are dereferenced)
97    fn set_attributes(&mut self, path: impl AsRef<Utf8Path>, attrs: SetAttrs) -> Result<()>;
98
99    /// Returns the path after following all symlinks, normalized and absolute
100    fn canonicalize(&self, path: impl AsRef<Utf8Path>) -> Result<Utf8PathBuf> {
101        let path = path.as_ref();
102        if !path.is_absolute() {
103            // TODO: Keep a current_directory to provide relative path support
104            bail!("Only absolute paths supported");
105        }
106        let mut canon = Utf8PathBuf::with_capacity(path.as_str().len());
107        for part in path.components() {
108            if part == Utf8Component::ParentDir {
109                let pop = canon.pop();
110                assert!(pop);
111                continue;
112            }
113            canon.push(part);
114            if self.is_link(Utf8Path::new(&canon)) {
115                let link = self.read_link(&canon)?;
116                if link.is_absolute() {
117                    canon.clear();
118                } else {
119                    canon.pop();
120                }
121                canon.push(link);
122                canon = self.canonicalize(canon)?;
123            }
124        }
125        Ok(canon)
126    }
127}
128
129/// Splits the dirname and basename of the path if possible to do so
130fn split(path: &Utf8Path) -> Option<(&Utf8Path, &str)> {
131    // TODO: Consider join(parent, "/absolute/child")
132    path.as_str().rsplit_once('/').map(|(parent, child)| {
133        if parent.is_empty() {
134            ("/".into(), child)
135        } else {
136            (parent.into(), child)
137        }
138    })
139}
140
141/// An absolute path that can be split easily into its [`Root`] and relative path parts
142pub struct PlantedPath {
143    root_len: usize,
144    full: Utf8PathBuf,
145}
146
147impl PlantedPath {
148    /// Creates a planted path from a given root and optional full path
149    ///
150    /// If no path is given the root's path will be used. If a given path is not prefixed with
151    /// the root's path, an error is returned.
152    pub fn new(root: &Root, path: Option<&Utf8Path>) -> Result<Self> {
153        let path = match path {
154            Some(path) => {
155                if !path.starts_with(root.path()) {
156                    bail!("Path {} must start with root {}", path, root.path());
157                }
158                path
159            }
160            None => root.path(),
161        };
162        Ok(PlantedPath {
163            root_len: root.path().as_str().len(),
164            full: path.to_owned(),
165        })
166    }
167
168    /// The absolute path of the root part of this planted path
169    pub fn root(&self) -> &Utf8Path {
170        self.full.as_str()[..self.root_len].into()
171    }
172
173    /// The full, absolute path
174    pub fn absolute(&self) -> &Utf8Path {
175        &self.full
176    }
177
178    /// The path relative to the root
179    pub fn relative(&self) -> &Utf8Path {
180        self.full.as_str()[self.root_len..]
181            .trim_start_matches('/')
182            .into()
183    }
184
185    /// Produces a new planted path with the given path part appended
186    pub fn join(&self, name: impl AsRef<str>) -> Result<Self> {
187        let name = name.as_ref();
188        if name.contains('/') {
189            bail!(
190                "Only single path components can be joined to a planted path: {}",
191                name
192            );
193        }
194        Ok(PlantedPath {
195            root_len: self.root_len,
196            full: self.full.join(name),
197        })
198    }
199}
200
201impl Display for PlantedPath {
202    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
203        write!(f, "{}", self.full)
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use anyhow::Result;
210
211    use super::*;
212
213    #[test]
214    fn check_relative() {
215        let path = PlantedPath::new(
216            &Root::try_from("/example").unwrap(),
217            Some(Utf8Path::new("/example/path")),
218        )
219        .unwrap();
220        assert_eq!(path.relative(), "path");
221    }
222
223    #[test]
224    fn canonicalize() -> Result<()> {
225        let path = Utf8Path::new("/");
226        let mut fs = MemoryFilesystem::new();
227        assert_eq!(fs.canonicalize(path).unwrap(), "/");
228
229        fs.create_directory("/dir", Default::default())?;
230        fs.create_symlink("/dir/sym", "../dir2/deeper")?;
231
232        //   /
233        //     dir/
234        //       sym -> ../dir2/deeper    (Doesn't exist so path is kept)
235
236        assert_eq!(fs.canonicalize("/dir/./sym//final")?, "/dir2/deeper/final");
237
238        fs.create_directory("/dir2", Default::default())?;
239        fs.create_directory("/dir2/deeper", Default::default())?;
240        fs.create_symlink("/dir2/deeper/final", "/end")?;
241
242        //   /
243        //     dir/
244        //       sym -> ../dir2/deeper    (Exists, so path is replaced)
245        //     dir2/
246        //       deeper/
247        //         final -> /end
248
249        assert_eq!(fs.canonicalize("/dir/./sym//final")?, "/end");
250
251        assert_eq!(fs.canonicalize("/dir/sym")?, "/dir2/deeper");
252        assert_eq!(fs.canonicalize("/dir/sym/.")?, "/dir2/deeper");
253        assert_eq!(fs.canonicalize("/dir/sym/..")?, "/dir2");
254
255        Ok(())
256    }
257}