diskplan_filesystem/
memory.rs

1use std::{
2    borrow::Cow,
3    collections::{HashMap, HashSet},
4};
5
6use anyhow::{anyhow, bail, Context, Result};
7use camino::{Utf8Path, Utf8PathBuf};
8use nix::unistd;
9use users::{Groups, Users, UsersCache};
10
11use super::{
12    attributes::Mode, Attrs, Filesystem, SetAttrs, DEFAULT_DIRECTORY_MODE, DEFAULT_FILE_MODE,
13};
14
15/// An in-memory representation of a file system
16pub struct MemoryFilesystem {
17    map: HashMap<Utf8PathBuf, Node>,
18    users: UsersCache,
19
20    uid: u32,
21    gid: u32,
22}
23
24#[derive(Debug)]
25enum Node {
26    File {
27        attrs: FSAttrs,
28        content: String,
29    },
30    Directory {
31        attrs: FSAttrs,
32        children: Vec<String>,
33    },
34    Symlink {
35        target: Utf8PathBuf,
36    },
37}
38
39#[derive(Debug)]
40struct FSAttrs {
41    uid: u32,
42    gid: u32,
43    mode: u16,
44}
45
46impl MemoryFilesystem {
47    const ROOT: u32 = 0;
48    const DEFAULT_OWNER: u32 = Self::ROOT;
49    const DEFAULT_GROUP: u32 = Self::ROOT;
50
51    /// Constructs a new in-memory filesystem
52    pub fn new() -> Self {
53        let mut map = HashMap::new();
54        map.insert(
55            "/".into(),
56            Node::Directory {
57                attrs: FSAttrs {
58                    uid: Self::DEFAULT_OWNER,
59                    gid: Self::DEFAULT_GROUP,
60                    mode: DEFAULT_DIRECTORY_MODE.into(),
61                },
62                children: vec![],
63            },
64        );
65        MemoryFilesystem {
66            map,
67            users: UsersCache::new(),
68            uid: unistd::getuid().as_raw(),
69            gid: unistd::getgid().as_raw(),
70        }
71    }
72
73    /// For use by tests to compare with expected results
74    pub fn to_path_set(&self) -> HashSet<&Utf8Path> {
75        self.map.keys().map(|i| i.as_ref()).collect()
76    }
77}
78
79impl Default for MemoryFilesystem {
80    fn default() -> Self {
81        Self::new()
82    }
83}
84
85impl Filesystem for MemoryFilesystem {
86    fn create_directory(&mut self, path: impl AsRef<Utf8Path>, attrs: SetAttrs) -> Result<()> {
87        let path = path.as_ref();
88        let (parent, name) = self
89            .canonical_split(path)
90            .with_context(|| format!("Splitting {path}"))?;
91        let attrs = self.internal_attrs(attrs, DEFAULT_DIRECTORY_MODE)?;
92        let children = vec![];
93        self.insert_node(&parent, name, Node::Directory { attrs, children })
94            .with_context(|| format!("Creating directory: {path}"))
95    }
96
97    fn create_file(
98        &mut self,
99        path: impl AsRef<Utf8Path>,
100        attrs: SetAttrs,
101        content: String,
102    ) -> Result<()> {
103        let path = path.as_ref();
104        let (parent, name) = self.canonical_split(path)?;
105        let attrs = self.internal_attrs(attrs, DEFAULT_FILE_MODE)?;
106        self.insert_node(&parent, name, Node::File { attrs, content })
107            .with_context(|| format!("Creating file: {path}"))
108    }
109
110    fn create_symlink(
111        &mut self,
112        path: impl AsRef<Utf8Path>,
113        target: impl AsRef<Utf8Path>,
114    ) -> Result<()> {
115        let path = path.as_ref();
116        let (parent, name) = self.canonical_split(path)?;
117        self.insert_node(
118            &parent,
119            name,
120            Node::Symlink {
121                target: target.as_ref().to_owned(),
122            },
123        )
124        .with_context(|| format!("Creating symlink: {path}"))
125    }
126
127    fn exists(&self, path: impl AsRef<Utf8Path>) -> bool {
128        match self.canonicalize(path) {
129            Ok(path) => self.map.contains_key(&path),
130            _ => false,
131        }
132    }
133
134    fn is_directory(&self, path: impl AsRef<Utf8Path>) -> bool {
135        match self.canonicalize(path) {
136            Err(_) => false,
137            Ok(path) => matches!(self.map.get(&path), Some(Node::Directory { .. })),
138        }
139    }
140
141    fn is_file(&self, path: impl AsRef<Utf8Path>) -> bool {
142        match self.canonicalize(path) {
143            Err(_) => false,
144            Ok(path) => matches!(self.map.get(&path), Some(Node::File { .. })),
145        }
146    }
147
148    fn is_link(&self, path: impl AsRef<Utf8Path>) -> bool {
149        matches!(self.map.get(path.as_ref()), Some(Node::Symlink { .. }))
150    }
151
152    fn list_directory(&self, path: impl AsRef<Utf8Path>) -> Result<Vec<String>> {
153        let path = self.canonicalize(path)?;
154        Ok(match self.node_from_path(&path)? {
155            Node::Directory { children, .. } => children.clone(),
156            Node::File { .. } => bail!("Tried to list directory of a file: {}", path),
157            Node::Symlink { .. } => unreachable!("Non-canonical path: {}", path),
158        })
159    }
160
161    fn read_file(&self, path: impl AsRef<Utf8Path>) -> Result<String> {
162        let path = self.canonicalize(path)?;
163        Ok(match self.node_from_path(&path)? {
164            Node::File { content, .. } => content.clone(),
165            Node::Directory { .. } => bail!("Tried to read directory as a file: {}", path),
166            Node::Symlink { .. } => unreachable!("Non-canonical path: {}", path),
167        })
168    }
169
170    fn read_link(&self, path: impl AsRef<Utf8Path>) -> Result<Utf8PathBuf> {
171        Ok(match self.node_from_path(&path)? {
172            Node::Symlink { target } => target.clone(),
173            _ => bail!("Not a symlink: {}", path.as_ref()),
174        })
175    }
176
177    fn attributes(&self, path: impl AsRef<Utf8Path>) -> Result<Attrs> {
178        let path = self.canonicalize(path)?;
179        let node = self.node_from_path(&path)?;
180        let attrs = match node {
181            Node::Directory { attrs, .. } | Node::File { attrs, .. } => attrs,
182            Node::Symlink { .. } => panic!("Non-canonical path: {path}"),
183        };
184        let owner = Cow::Owned(
185            self.users
186                .get_user_by_uid(attrs.uid)
187                .ok_or_else(|| anyhow!("Failed to get user from UID: {}", attrs.uid))?
188                .name()
189                .to_string_lossy()
190                .into_owned(),
191        );
192        let group = Cow::Owned(
193            self.users
194                .get_group_by_gid(attrs.gid)
195                .ok_or_else(|| anyhow!("Failed to get group from GID: {}", attrs.gid))?
196                .name()
197                .to_string_lossy()
198                .into_owned(),
199        );
200        let mode = attrs.mode.into();
201        Ok(Attrs { owner, group, mode })
202    }
203
204    fn set_attributes(&mut self, path: impl AsRef<Utf8Path>, set_attrs: SetAttrs) -> Result<()> {
205        let use_default = set_attrs.mode.is_none();
206        let mut fs_attrs = self.internal_attrs(set_attrs, 0.into())?;
207        let path = self.canonicalize(path)?;
208        let node = self
209            .map
210            .get_mut(&path)
211            .ok_or_else(|| anyhow!("No such file or directory: {}", path))?;
212        match node {
213            Node::Directory { attrs, .. } => {
214                if use_default {
215                    fs_attrs.mode = DEFAULT_DIRECTORY_MODE.into();
216                }
217                *attrs = fs_attrs;
218                Ok(())
219            }
220            Node::File { attrs, .. } => {
221                if use_default {
222                    fs_attrs.mode = DEFAULT_FILE_MODE.into();
223                }
224                *attrs = fs_attrs;
225                Ok(())
226            }
227            Node::Symlink { .. } => Err(anyhow!("Non-canonical path: {}", path)),
228        }
229    }
230}
231
232impl MemoryFilesystem {
233    fn canonical_split<'s>(&self, path: &'s Utf8Path) -> Result<(Utf8PathBuf, &'s str)> {
234        match super::split(path) {
235            None => Err(anyhow!("Cannot create {}", path)),
236            Some((parent, name)) => Ok((self.canonicalize(parent)?, name)),
237        }
238    }
239
240    fn internal_attrs(&self, attrs: SetAttrs, default_mode: Mode) -> Result<FSAttrs> {
241        let uid = match attrs.owner {
242            Some(owner) => self
243                .users
244                .get_user_by_name(owner)
245                .ok_or_else(|| anyhow!("No such user: {}", owner))?
246                .uid(),
247            None => self.uid,
248        };
249        let gid = match attrs.group {
250            Some(group) => self
251                .users
252                .get_group_by_name(group)
253                .ok_or_else(|| anyhow!("No such group: {}", group))?
254                .gid(),
255            None => self.gid,
256        };
257        let mode = attrs.mode.unwrap_or(default_mode).into();
258        Ok(FSAttrs { uid, gid, mode })
259    }
260
261    /// Inserts a new entry into the filesystem, under the given *canonical* parent
262    ///
263    /// # Arguments
264    ///
265    /// * `parent` - A canonical path to the parent directory of the entry
266    /// * `name` - The name to give to the new entry
267    /// * `node` - The entry itself
268    ///
269    fn insert_node(&mut self, parent: impl AsRef<Utf8Path>, name: &str, node: Node) -> Result<()> {
270        // Check it doesn't already exist
271        let parent = parent.as_ref();
272        let path = parent.join(name);
273        if self.map.contains_key(&path) {
274            bail!("File exists: {:?}", path);
275        }
276        let parent_node = self
277            .map
278            .get_mut(parent)
279            .ok_or_else(|| anyhow!("Parent directory not found: {}", parent))?;
280        // Insert name into parent
281        match parent_node {
282            Node::Directory {
283                ref mut children, ..
284            } => children.push(name.into()),
285            _ => panic!("Parent not a directory: {parent}"),
286        }
287        // Insert full path and node into map
288        self.map.insert(path, node);
289        Ok(())
290    }
291
292    fn node_from_path(&self, path: impl AsRef<Utf8Path>) -> Result<&Node> {
293        let path = path.as_ref();
294        self.map
295            .get(path)
296            .ok_or_else(|| anyhow!("No such file or directory: {}", path))
297    }
298}
299
300#[cfg(test)]
301mod tests {
302    use crate::{Filesystem, SetAttrs};
303
304    use super::MemoryFilesystem;
305
306    #[test]
307    fn exists() {
308        let mut fs = MemoryFilesystem::new();
309        assert!(fs.exists("/"));
310        assert!(!fs.exists("/entry"));
311        fs.create_directory("/entry", SetAttrs::default()).unwrap();
312        assert!(fs.exists("/entry"));
313    }
314
315    #[test]
316    fn symlink_make_sub_directory() {
317        let mut fs = MemoryFilesystem::new();
318        fs.create_directory("/primary", SetAttrs::default())
319            .unwrap();
320        fs.create_directory("/secondary", SetAttrs::default())
321            .unwrap();
322        fs.create_symlink("/primary/link", "/secondary/target")
323            .unwrap();
324        fs.create_directory("/secondary/target", SetAttrs::default())
325            .unwrap();
326        fs.create_directory("/primary/link/through", SetAttrs::default())
327            .unwrap();
328        assert!(fs.exists("/primary/link/through"));
329    }
330}