wasi_vfs/embed/
mod.rs

1//! This module provides an in-memory filesystem implementation.
2
3mod linked_storage;
4pub use linked_storage::LinkedStorage;
5
6use crate::Vfd;
7use std::{collections::HashMap, path::Path};
8
9pub(crate) trait NodeIdTrait {
10    fn ino(&self) -> u64;
11}
12
13pub(crate) trait NodeFileBody {
14    fn content(&self) -> &[u8];
15}
16
17pub(crate) struct DirEntry<S: Storage + ?Sized> {
18    pub(crate) name: String,
19    pub(crate) link_id: S::LinkId,
20}
21
22impl<S: Storage> Clone for DirEntry<S> {
23    fn clone(&self) -> Self {
24        DirEntry {
25            name: self.name.clone(),
26            link_id: self.link_id,
27        }
28    }
29}
30
31pub(crate) trait NodeDirBody<S: Storage + ?Sized> {
32    type Iter: Iterator<Item = DirEntry<S>>;
33    fn entries(&self) -> Self::Iter;
34}
35
36/// A storage that can be used to store files and directories.
37pub(crate) trait Storage {
38    type NodeId: NodeIdTrait + Clone + Copy;
39    type LinkId: Clone + Copy;
40    type NodeFileBody: NodeFileBody;
41    type NodeDirBody: NodeDirBody<Self>;
42
43    /// Creates a new root node.
44    fn new_root_dir(&mut self) -> (Self::NodeId, Self::LinkId);
45
46    /// Creates a new directory node under the given parent node.
47    fn new_dir(
48        &mut self,
49        parent: (Self::NodeId, Self::LinkId),
50        name: String,
51    ) -> (Self::NodeId, Self::LinkId);
52
53    /// Creates a new file node under the given parent node.
54    fn new_file(
55        &mut self,
56        parent: (Self::NodeId, Self::LinkId),
57        name: String,
58        content: Vec<u8>,
59    ) -> (Self::NodeId, Self::LinkId);
60
61    /// Resolve a node from its id.
62    fn get_inode(&self, node_id: &Self::NodeId) -> Node<Self>;
63
64    /// Resolve a link from its id.
65    fn get_link(&self, link_id: &Self::LinkId) -> Link<Self>;
66
67    /// Resolve a node from base node and relative path.
68    fn resolve_node(
69        &self,
70        base: Self::NodeId,
71        base_link: Self::LinkId,
72        path: &Path,
73    ) -> Result<(Self::NodeId, Self::LinkId), wasi::Errno>;
74}
75
76pub(crate) enum Node<'a, S: Storage + ?Sized> {
77    File(&'a S::NodeFileBody),
78    Dir(&'a S::NodeDirBody),
79}
80
81/// Represent a hard link to an inode
82pub(crate) struct Link<S: Storage + ?Sized> {
83    pub(crate) parent: Option<S::LinkId>,
84    pub(crate) node: S::NodeId,
85}
86
87impl<S: Storage> Clone for Link<S> {
88    fn clone(&self) -> Self {
89        Link {
90            parent: self.parent,
91            node: self.node,
92        }
93    }
94}
95
96impl<S: Storage> Copy for Link<S> {}
97
98pub(crate) struct FdEntry<S: Storage + ?Sized> {
99    pub(crate) offset: usize,
100    pub(crate) link_id: S::LinkId,
101    pub(crate) node_id: S::NodeId,
102    pub(crate) flags: wasi::Fdflags,
103}
104
105pub(crate) struct PreopenedDir {
106    pub(crate) path: String,
107}
108
109pub(crate) struct EmbeddedFs<S: Storage> {
110    preopened_dirs: Vec<PreopenedDir>,
111    storage: S,
112
113    opens: HashMap<Vfd, FdEntry<S>>,
114    fd_issuer: IdIssuer<Vfd>,
115}
116
117#[derive(Default)]
118struct IdIssuer<Id> {
119    next_id: Id,
120}
121
122impl<Id> IdIssuer<Id> {
123    fn new(base: Id) -> Self {
124        Self { next_id: base }
125    }
126}
127
128impl<Id: std::ops::AddAssign<u32> + Clone> IdIssuer<Id> {
129    fn issue(&mut self) -> Id {
130        let id = self.next_id.clone();
131        self.next_id += 1;
132        id
133    }
134}
135
136impl<S: Storage> Default for EmbeddedFs<S>
137where
138    S: Default,
139{
140    fn default() -> Self {
141        Self::new(S::default())
142    }
143}
144
145impl<S: Storage> EmbeddedFs<S> {
146    pub(crate) fn new(storage: S) -> Self {
147        Self {
148            preopened_dirs: vec![],
149            storage,
150            opens: HashMap::new(),
151            fd_issuer: IdIssuer::new(0_u32),
152        }
153    }
154
155    pub(crate) fn preopen_dir(&mut self, path: String) -> (Vfd, S::NodeId, S::LinkId) {
156        assert!(self.preopened_dirs.len() == self.opens.len());
157        let fd = self.fd_issuer.issue();
158        self.preopened_dirs.push(PreopenedDir { path });
159        let (node_id, link_id) = self.storage.new_root_dir();
160        self.opens.insert(
161            fd,
162            FdEntry {
163                offset: 0,
164                node_id,
165                link_id,
166                flags: 0,
167            },
168        );
169        (fd, node_id, link_id)
170    }
171
172    pub(crate) fn get_preopened_dir_path(&self, vfd: Vfd) -> Option<&str> {
173        let vfd = vfd as usize;
174        if vfd >= self.preopened_dirs.len() {
175            return None;
176        }
177        Some(&self.preopened_dirs[vfd].path)
178    }
179
180    pub(crate) fn create_dir(
181        &mut self,
182        dir_node: S::NodeId,
183        dir_link: S::LinkId,
184        relpath: &str,
185    ) -> Result<(), u16> {
186        let (cursor, filename) = self.create_intermediate_dirs(dir_node, dir_link, relpath)?;
187        self.storage.new_dir(cursor, filename.to_string());
188        Ok(())
189    }
190
191    pub(crate) fn create_file(
192        &mut self,
193        dir_node: S::NodeId,
194        dir_link: S::LinkId,
195        relpath: &str,
196        content: Vec<u8>,
197    ) -> Result<(), u16> {
198        let (cursor, filename) = self.create_intermediate_dirs(dir_node, dir_link, relpath)?;
199        self.storage.new_file(cursor, filename.to_string(), content);
200        Ok(())
201    }
202
203    fn create_intermediate_dirs<'path>(
204        &mut self,
205        base_node: S::NodeId,
206        base_link: S::LinkId,
207        mut relpath: &'path str,
208    ) -> Result<((S::NodeId, S::LinkId), &'path str), u16> {
209        let mut cursor = match self.storage.get_inode(&base_node) {
210            Node::Dir { .. } => (base_node, base_link),
211            _ => return Err(wasi::ERRNO_BADF.raw()),
212        };
213        if relpath.starts_with('/') {
214            relpath = &relpath[1..];
215        }
216        let components = relpath.split('/').collect::<Vec<_>>();
217        let filename = match components.last() {
218            Some(filename) => *filename,
219            None => return Err(wasi::ERRNO_NOENT.raw()),
220        };
221
222        let components_len = components.len();
223
224        'find_parent_node: for component in components.into_iter().take(components_len - 1) {
225            if component == "." {
226                continue;
227            }
228            let entries = match self.storage.get_inode(&cursor.0) {
229                Node::Dir(body) => body.entries(),
230                _ => return Err(wasi::ERRNO_BADF.raw()),
231            };
232            for entry in entries {
233                if component == entry.name {
234                    cursor = (self.storage.get_link(&entry.link_id).node, entry.link_id);
235                    continue 'find_parent_node;
236                }
237            }
238            // create a new intermediate directory
239            {
240                let (new_dir_id, new_link_id) = self.storage.new_dir(cursor, component.to_string());
241                cursor = (new_dir_id, new_link_id);
242            }
243        }
244        Ok(((cursor.0, cursor.1), filename))
245    }
246
247    pub(crate) fn get_node_id_by_link(&self, id: S::LinkId) -> S::NodeId {
248        self.storage.get_link(&id).node
249    }
250
251    pub(crate) fn get_node(&self, fd: Vfd) -> Result<Node<S>, wasi::Errno> {
252        match self.opens.get(&fd) {
253            Some(entry) => Ok(self.storage.get_inode(&entry.node_id)),
254            None => Err(wasi::ERRNO_BADF),
255        }
256    }
257
258    pub(crate) fn get_fd_stat(&self, fd: Vfd) -> Result<wasi::Fdstat, wasi::Errno> {
259        const READ_ONLY_RIGHTS: wasi::Rights = wasi::RIGHTS_FD_READ
260            | wasi::RIGHTS_FD_ADVISE
261            | wasi::RIGHTS_PATH_OPEN
262            | wasi::RIGHTS_FD_READDIR
263            | wasi::RIGHTS_FD_FILESTAT_GET;
264        let entry = match self.opens.get(&fd) {
265            Some(entry) => entry,
266            None => return Err(wasi::ERRNO_BADF),
267        };
268        Ok(match self.storage.get_inode(&entry.node_id) {
269            Node::File { .. } => wasi::Fdstat {
270                fs_filetype: wasi::FILETYPE_REGULAR_FILE,
271                fs_flags: entry.flags,
272                fs_rights_base: READ_ONLY_RIGHTS,
273                fs_rights_inheriting: READ_ONLY_RIGHTS,
274            },
275            Node::Dir { .. } => wasi::Fdstat {
276                fs_filetype: wasi::FILETYPE_DIRECTORY,
277                fs_flags: entry.flags,
278                fs_rights_base: READ_ONLY_RIGHTS,
279                fs_rights_inheriting: READ_ONLY_RIGHTS,
280            },
281        })
282    }
283
284    pub(crate) fn get_filestat_from_node_id(&self, node_id: S::NodeId) -> wasi::Filestat {
285        let mut stat = wasi::Filestat {
286            dev: Default::default(),
287            ino: Default::default(),
288            filetype: wasi::FILETYPE_UNKNOWN,
289            nlink: Default::default(),
290            size: Default::default(),
291            atim: Default::default(),
292            mtim: Default::default(),
293            ctim: Default::default(),
294        };
295        stat.ino = node_id.ino();
296        match self.storage.get_inode(&node_id) {
297            Node::File(body) => {
298                stat.filetype = wasi::FILETYPE_REGULAR_FILE;
299                stat.size = body.content().len() as u64;
300                stat
301            }
302            Node::Dir { .. } => {
303                stat.filetype = wasi::FILETYPE_DIRECTORY;
304                stat
305            }
306        }
307    }
308
309    pub(crate) fn get_fd_entry_mut(&mut self, fd: Vfd) -> Result<&mut FdEntry<S>, wasi::Errno> {
310        match self.opens.get_mut(&fd) {
311            Some(open_file) => Ok(open_file),
312            None => Err(wasi::ERRNO_BADF),
313        }
314    }
315
316    pub(crate) fn get_fd_entry(&self, fd: Vfd) -> Result<&FdEntry<S>, wasi::Errno> {
317        match self.opens.get(&fd) {
318            Some(open_file) => Ok(open_file),
319            None => Err(wasi::ERRNO_BADF),
320        }
321    }
322
323    pub(crate) fn close_file(&mut self, fd: Vfd) -> Result<(), wasi::Errno> {
324        match self.opens.remove(&fd) {
325            Some(_) => Ok(()),
326            None => Err(wasi::ERRNO_BADF),
327        }
328    }
329
330    pub(crate) fn open_file(
331        &mut self,
332        base: Vfd,
333        path: &Path,
334        fdflags: wasi::Fdflags,
335    ) -> Result<Vfd, wasi::Errno> {
336        let base = &self.opens[&base];
337        let (node_id, link_id) = self
338            .storage
339            .resolve_node(base.node_id, base.link_id, path)?;
340        let new_fd = self.fd_issuer.issue();
341        self.opens.insert(
342            new_fd,
343            FdEntry {
344                offset: 0,
345                node_id,
346                link_id,
347                flags: fdflags,
348            },
349        );
350        Ok(new_fd)
351    }
352
353    pub(crate) fn get_filestat_at_path(
354        &self,
355        base: Vfd,
356        path: &Path,
357    ) -> Result<wasi::Filestat, wasi::Errno> {
358        let base = &self.opens[&base];
359        let (node_id, _) = self
360            .storage
361            .resolve_node(base.node_id, base.link_id, path)?;
362        let res = self.get_filestat_from_node_id(node_id);
363        Ok(res)
364    }
365}
366
367#[cfg(test)]
368mod tests {
369    use std::path::Path;
370
371    use super::{EmbeddedFs, LinkedStorage};
372
373    #[test]
374    fn test_embedded_node_create_file() {
375        let content = "Hello".as_bytes().to_vec();
376        let mut fs = EmbeddedFs::<LinkedStorage>::default();
377        let (_, node_id, link_id) = fs.preopen_dir("/".to_string());
378        fs.create_file(node_id, link_id, "hello.txt", content)
379            .unwrap();
380    }
381
382    #[test]
383    fn test_get_filestat_at_path_for_non_existing() {
384        let mut fs = EmbeddedFs::<LinkedStorage>::default();
385        let (vfd, _, _) = fs.preopen_dir("/".to_string());
386        let result = fs.get_filestat_at_path(vfd, Path::new("/not-exist"));
387        assert!(result.is_err());
388    }
389}