mini_fs/
lib.rs

1//! `mini-fs` is an extensible virtual filesystem for the application layer.
2//!
3//! Currently supported features include:
4//!
5//! - Access to the local (native) filesystem.
6//! - In-memory filesystems.
7//! - Read from tar, tar.gz, and zip archives.
8//! - Filesystem overlays.
9//!
10//! ## Case sensitivity
11//!
12//! All implementations of [`Store`] from this crate use **case sensitive**¹
13//! paths. However, you are free to implement custom stores where paths are case
14//! insensitive.
15//!
16//! ¹ Except maybe [`LocalFs`], which uses [`std::fs`] internally and is subject
17//! to the underlying OS.
18//!
19//! ## Example
20//!
21//! ```no_run
22//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
23//! use mini_fs::prelude::*;
24//! use mini_fs::{LocalFs, MiniFs, ZipFs};
25//!
26//! let gfx = LocalFs::new("./res/images");
27//! let sfx = ZipFs::open("archive.zip")?;
28//!
29//! let assets = MiniFs::new().mount("/gfx", gfx).mount("/sfx", sfx);
30//!
31//! let root = MiniFs::new().mount("/assets", assets);
32//!
33//! let file = root.open("/assets/gfx/trash.gif")?;
34//! # Ok(())
35//! # }
36//! ```
37//!
38//! ## Security
39//!
40//! Don't use this crate in applications where security is a critical factor.
41//! [`LocalFs`] in particular might be vulnerable to [directory traversal
42//! attacks][dir], so it's best not to use it directly in a static file server,
43//! for example.
44//!
45//! [`std::fs`]: https://doc.rust-lang.org/std/fs/index.html
46//! [`Store`]: ./trait.Store.html
47//! [`LocalFs`]: ./struct.LocalFs.html
48//! [dir]: https://en.wikipedia.org/wiki/Directory_traversal_attack
49#![deny(warnings)]
50use std::any::Any;
51use std::collections::LinkedList;
52use std::io::{Cursor, Error, ErrorKind, Read, Result, Seek, SeekFrom};
53use std::path::{Path, PathBuf};
54use std::rc::Rc;
55use std::{env, fs};
56
57pub use caseless::CaselessFs;
58//pub use index::{Index, IndexEntries};
59pub use store::{Entries, Entry, EntryKind, Store, StoreExt};
60#[cfg(feature = "tar")]
61pub use tar::TarFs;
62#[cfg(feature = "zip")]
63pub use zip::ZipFs;
64
65include!("macros.rs");
66
67pub mod caseless;
68/// Directory index.
69#[doc(hidden)]
70pub mod index;
71mod store;
72/// Tar file storage.
73#[cfg(feature = "tar")]
74pub mod tar;
75/// Zip file storage.
76#[cfg(feature = "zip")]
77pub mod zip;
78/// Convenient library imports.
79pub mod prelude {
80    pub use crate::store::{Store, StoreExt};
81}
82
83impl_file! {
84    /// File you can seek and read from.
85    pub enum File {
86        Local(fs::File),
87        Ram(RamFile),
88        #[cfg(feature = "zip")]
89        Zip(zip::ZipFsFile),
90        #[cfg(feature = "tar")]
91        Tar(tar::TarFsFile),
92        // External types are dynamic
93        User(Box<dyn UserFile>),
94    }
95}
96
97/// Custom file type.
98pub trait UserFile: Any + Read + Seek + Send {}
99
100impl<T: UserFile> From<T> for File {
101    fn from(file: T) -> Self {
102        File::User(Box::new(file))
103    }
104}
105
106struct Mount {
107    path: PathBuf,
108    store: Box<dyn Store<File = File>>,
109}
110
111/// Virtual filesystem.
112pub struct MiniFs {
113    mount: LinkedList<Mount>,
114}
115
116impl Store for MiniFs {
117    type File = File;
118
119    fn open_path(&self, path: &Path) -> Result<File> {
120        let next = self.mount.iter().rev().find_map(|mnt| {
121            if let Ok(np) = path.strip_prefix(&mnt.path) {
122                Some((np, &mnt.store))
123            } else {
124                None
125            }
126        });
127        if let Some((np, store)) = next {
128            store.open_path(np)
129        } else {
130            Err(Error::from(ErrorKind::NotFound))
131        }
132    }
133
134    fn entries_path(&self, path: &Path) -> Result<Entries> {
135        // FIXME creating a new PathBuf because otherwise I'm getting lifetime errors.
136        let path = path.to_path_buf();
137
138        Ok(Entries::new(
139            self.mount
140                .iter()
141                .rev()
142                .find(|m| path.strip_prefix(&m.path).is_ok())
143                .into_iter()
144                .flat_map(move |m| match path.strip_prefix(&m.path) {
145                    Ok(np) => m.store.entries_path(np).unwrap(),
146                    Err(_) => Entries::new(None),
147                }),
148        ))
149    }
150}
151
152impl MiniFs {
153    pub fn new() -> Self {
154        Self {
155            mount: LinkedList::new(),
156        }
157    }
158
159    pub fn mount<P, S, T>(mut self, path: P, store: S) -> Self
160    where
161        P: Into<PathBuf>,
162        S: Store<File = T> + 'static,
163        T: Into<File>,
164    {
165        let path = path.into();
166        let store = Box::new(store::MapFile::new(store, |file: T| file.into()));
167        self.mount.push_back(Mount { path, store });
168        self
169    }
170
171    pub fn umount<P>(&mut self, path: P) -> Option<Box<dyn Store<File = File>>>
172    where
173        P: AsRef<Path>,
174    {
175        let path = path.as_ref();
176        if let Some(p) = self.mount.iter().rposition(|p| p.path == path) {
177            let mut tail = self.mount.split_off(p);
178            let fs = tail.pop_front().map(|m| m.store);
179            self.mount.append(&mut tail);
180            fs
181        } else {
182            None
183        }
184    }
185}
186
187/// Native file store.
188pub struct LocalFs {
189    root: PathBuf,
190}
191
192impl Store for LocalFs {
193    type File = fs::File;
194
195    fn open_path(&self, path: &Path) -> Result<fs::File> {
196        fs::OpenOptions::new()
197            .create(false)
198            .read(true)
199            .write(false)
200            .open(self.root.join(path))
201    }
202
203    fn entries_path(&self, path: &Path) -> Result<Entries> {
204        // FIXME cloned because lifetimes.
205        //let root = self.root.clone();
206
207        let entries = fs::read_dir(self.root.join(path))?.map(move |ent| {
208            let entry = ent?;
209            let path = entry
210                .path()
211                .strip_prefix(&self.root)
212                .map(Path::to_path_buf)
213                .expect("Error striping path suffix.");
214            let file_type = entry.file_type()?;
215
216            // TODO synlinks
217            let kind = if file_type.is_dir() {
218                EntryKind::Dir
219            } else if file_type.is_symlink() {
220                EntryKind::File
221            } else {
222                EntryKind::File
223            };
224
225            Ok(Entry {
226                name: path.into_os_string(),
227                kind,
228            })
229        });
230
231        Ok(Entries::new(entries))
232    }
233}
234
235impl LocalFs {
236    pub fn new<P: Into<PathBuf>>(root: P) -> Self {
237        Self { root: root.into() }
238    }
239
240    /// Point to the current working directory.
241    pub fn pwd() -> Result<Self> {
242        Ok(Self::new(env::current_dir()?))
243    }
244}
245
246/// In-memory file storage
247pub struct RamFs {
248    index: index::Index<Rc<[u8]>>,
249}
250
251/// In-memory file.
252pub struct RamFile(Cursor<Rc<[u8]>>);
253
254impl Read for RamFile {
255    fn read(&mut self, buf: &mut [u8]) -> Result<usize> {
256        self.0.read(buf)
257    }
258}
259
260impl Seek for RamFile {
261    fn seek(&mut self, pos: SeekFrom) -> Result<u64> {
262        self.0.seek(pos)
263    }
264}
265
266impl Store for RamFs {
267    type File = RamFile;
268
269    fn open_path(&self, path: &Path) -> Result<Self::File> {
270        match self.index.get(path) {
271            Some(file) => Ok(RamFile(Cursor::new(Rc::clone(file)))),
272            None => Err(Error::from(ErrorKind::NotFound)),
273        }
274    }
275
276    fn entries_path(&self, path: &Path) -> Result<Entries> {
277        Ok(Entries::new(self.index.entries(path).map(|ent| {
278            Ok(Entry {
279                name: ent.name.to_os_string(),
280                kind: ent.kind,
281            })
282        })))
283    }
284}
285
286impl RamFs {
287    pub fn new() -> Self {
288        Self {
289            index: index::Index::new(),
290        }
291    }
292
293    pub fn clear(&mut self) {
294        self.index.clear();
295    }
296
297    pub fn rm<P: AsRef<Path>>(&mut self, path: P) -> Option<Rc<[u8]>> {
298        self.index.remove(path)
299    }
300
301    pub fn touch<P, F>(&mut self, path: P, file: F)
302    where
303        P: Into<PathBuf>,
304        F: Into<Rc<[u8]>>,
305    {
306        self.index.insert(path.into(), file.into());
307    }
308
309    pub fn index(self) -> Self {
310        self
311    }
312}