1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
//! Provides an abstract [`Filesystem`] trait, together with a physical ([`DiskFilesystem`])
//! and virtual ([`MemoryFilesystem`]) implementation.
#![warn(missing_docs)]

use std::fmt::Display;

use anyhow::{bail, Result};
use camino::{Utf8Component, Utf8Path, Utf8PathBuf};

mod attributes;
mod memory;
mod physical;
mod root;

pub use self::{
    attributes::{Attrs, Mode, SetAttrs, DEFAULT_DIRECTORY_MODE, DEFAULT_FILE_MODE},
    memory::MemoryFilesystem,
    physical::DiskFilesystem,
    root::Root,
};

impl SetAttrs<'_> {
    /// Returns true if this `SetAttrs` matches the given, existing `attrs`
    pub fn matches(&self, attrs: &Attrs) -> bool {
        let SetAttrs { owner, group, mode } = self;
        owner.map(|owner| owner == attrs.owner).unwrap_or(true)
            && group.map(|group| group == attrs.group).unwrap_or(true)
            && mode.map(|mode| mode == attrs.mode).unwrap_or(true)
    }
}

/// Operations of a file system
pub trait Filesystem {
    /// Create a directory at the given path, with any number of attributes set
    fn create_directory(&mut self, path: impl AsRef<Utf8Path>, attrs: SetAttrs) -> Result<()>;

    /// Create a directory and all of its parents
    fn create_directory_all(&mut self, path: impl AsRef<Utf8Path>, attrs: SetAttrs) -> Result<()> {
        let path = path.as_ref();
        if let Some((parent, _)) = split(path) {
            if parent != "/" {
                self.create_directory_all(parent, attrs.clone())?;
            }
        }
        if !self.is_directory(path) {
            self.create_directory(path, attrs)?;
        }
        Ok(())
    }

    /// Create a file with the given content and any number of attributes set
    fn create_file(
        &mut self,
        path: impl AsRef<Utf8Path>,
        attrs: SetAttrs,
        content: String,
    ) -> Result<()>;

    /// Create a symlink pointing to the given target
    fn create_symlink(
        &mut self,
        path: impl AsRef<Utf8Path>,
        target: impl AsRef<Utf8Path>,
    ) -> Result<()>;

    /// Returns true if the path exists
    fn exists(&self, path: impl AsRef<Utf8Path>) -> bool;

    /// Returns true if the path is a directory
    fn is_directory(&self, path: impl AsRef<Utf8Path>) -> bool;

    /// Returns true if the path is a regular file
    fn is_file(&self, path: impl AsRef<Utf8Path>) -> bool;

    /// Returns true if the path is a symbolic link
    fn is_link(&self, path: impl AsRef<Utf8Path>) -> bool;

    /// Lists the contents of the given directory
    fn list_directory(&self, path: impl AsRef<Utf8Path>) -> Result<Vec<String>>;

    /// Reads the contents of the given file
    fn read_file(&self, path: impl AsRef<Utf8Path>) -> Result<String>;

    /// Reads the path pointed to by the given symbolic link
    fn read_link(&self, path: impl AsRef<Utf8Path>) -> Result<Utf8PathBuf>;

    /// Returns the attributes of the given file, directory
    ///
    /// If the path is a symlink, the file/directory pointed to by the symlink will be checked
    /// and its attributes returned (i.e. paths are dereferenced)
    fn attributes(&self, path: impl AsRef<Utf8Path>) -> Result<Attrs>;

    /// Sets the attributes of the given file or directory
    ///
    /// If the path is a symlink, the file/directory pointed to by the symlink will be updated
    /// with the given attributes (i.e. paths are dereferenced)
    fn set_attributes(&mut self, path: impl AsRef<Utf8Path>, attrs: SetAttrs) -> Result<()>;

    /// Returns the path after following all symlinks, normalized and absolute
    fn canonicalize(&self, path: impl AsRef<Utf8Path>) -> Result<Utf8PathBuf> {
        let path = path.as_ref();
        if !path.is_absolute() {
            // TODO: Keep a current_directory to provide relative path support
            bail!("Only absolute paths supported");
        }
        let mut canon = Utf8PathBuf::with_capacity(path.as_str().len());
        for part in path.components() {
            if part == Utf8Component::ParentDir {
                let pop = canon.pop();
                assert!(pop);
                continue;
            }
            canon.push(part);
            if self.is_link(Utf8Path::new(&canon)) {
                let link = self.read_link(&canon)?;
                if link.is_absolute() {
                    canon.clear();
                } else {
                    canon.pop();
                }
                canon.push(link);
                canon = self.canonicalize(canon)?;
            }
        }
        Ok(canon)
    }
}

/// Splits the dirname and basename of the path if possible to do so
fn split(path: &Utf8Path) -> Option<(&Utf8Path, &str)> {
    // TODO: Consider join(parent, "/absolute/child")
    path.as_str().rsplit_once('/').map(|(parent, child)| {
        if parent.is_empty() {
            ("/".into(), child)
        } else {
            (parent.into(), child)
        }
    })
}

/// An absolute path that can be split easily into its [`Root`] and relative path parts
pub struct PlantedPath {
    root_len: usize,
    full: Utf8PathBuf,
}

impl PlantedPath {
    /// Creates a planted path from a given root and optional full path
    ///
    /// If no path is given the root's path will be used. If a given path is not prefixed with
    /// the root's path, an error is returned.
    pub fn new(root: &Root, path: Option<&Utf8Path>) -> Result<Self> {
        let path = match path {
            Some(path) => {
                if !path.starts_with(root.path()) {
                    bail!("Path {} must start with root {}", path, root.path());
                }
                path
            }
            None => root.path(),
        };
        Ok(PlantedPath {
            root_len: root.path().as_str().len(),
            full: path.to_owned(),
        })
    }

    /// The absolute path of the root part of this planted path
    pub fn root(&self) -> &Utf8Path {
        self.full.as_str()[..self.root_len].into()
    }

    /// The full, absolute path
    pub fn absolute(&self) -> &Utf8Path {
        &self.full
    }

    /// The path relative to the root
    pub fn relative(&self) -> &Utf8Path {
        self.full.as_str()[self.root_len..]
            .trim_start_matches('/')
            .into()
    }

    /// Produces a new planted path with the given path part appended
    pub fn join(&self, name: impl AsRef<str>) -> Result<Self> {
        let name = name.as_ref();
        if name.contains('/') {
            bail!(
                "Only single path components can be joined to a planted path: {}",
                name
            );
        }
        Ok(PlantedPath {
            root_len: self.root_len,
            full: self.full.join(name),
        })
    }
}

impl Display for PlantedPath {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.full)
    }
}

#[cfg(test)]
mod tests {
    use anyhow::Result;

    use super::*;

    #[test]
    fn check_relative() {
        let path = PlantedPath::new(
            &Root::try_from("/example").unwrap(),
            Some(Utf8Path::new("/example/path")),
        )
        .unwrap();
        assert_eq!(path.relative(), "path");
    }

    #[test]
    fn canonicalize() -> Result<()> {
        let path = Utf8Path::new("/");
        let mut fs = MemoryFilesystem::new();
        assert_eq!(fs.canonicalize(path).unwrap(), "/");

        fs.create_directory("/dir", Default::default())?;
        fs.create_symlink("/dir/sym", "../dir2/deeper")?;

        //   /
        //     dir/
        //       sym -> ../dir2/deeper    (Doesn't exist so path is kept)

        assert_eq!(fs.canonicalize("/dir/./sym//final")?, "/dir2/deeper/final");

        fs.create_directory("/dir2", Default::default())?;
        fs.create_directory("/dir2/deeper", Default::default())?;
        fs.create_symlink("/dir2/deeper/final", "/end")?;

        //   /
        //     dir/
        //       sym -> ../dir2/deeper    (Exists, so path is replaced)
        //     dir2/
        //       deeper/
        //         final -> /end

        assert_eq!(fs.canonicalize("/dir/./sym//final")?, "/end");

        assert_eq!(fs.canonicalize("/dir/sym")?, "/dir2/deeper");
        assert_eq!(fs.canonicalize("/dir/sym/.")?, "/dir2/deeper");
        assert_eq!(fs.canonicalize("/dir/sym/..")?, "/dir2");

        Ok(())
    }
}