dir_structure/vfs/
include_dir_vfs.rs

1//! A [`Vfs`] implementation for an [`include_dir::Dir`] directory.
2
3use core::fmt;
4use core::slice;
5use std::error;
6use std::io;
7use std::path::Component;
8use std::path::Path;
9use std::path::PathBuf;
10use std::pin::Pin;
11use std::result::Result as StdResult;
12
13use include_dir::Dir;
14use include_dir::DirEntry;
15#[cfg(doc)]
16use include_dir::include_dir as _include_dir;
17
18use crate::error::Error;
19use crate::error::Result;
20use crate::error::VfsResult;
21use crate::traits::vfs::DirEntryInfo;
22use crate::traits::vfs::DirEntryKind;
23use crate::traits::vfs::DirWalker;
24use crate::traits::vfs::PathType;
25use crate::traits::vfs::Vfs;
26use crate::traits::vfs::VfsCore;
27
28/// A [`Vfs`] implementation with an [`include_dir::Dir`] directory.
29pub struct IncludeDirVfs {
30    dir: Dir<'static>,
31}
32
33impl IncludeDirVfs {
34    /// Creates a new [`IncludeDirVfs`].
35    pub fn new(dir: Dir<'static>) -> Self {
36        Self { dir }
37    }
38}
39
40/// Convenience macro to [`include_dir!(...)`][include_dir] and wrap it in an [`IncludeDirVfs`].
41///
42/// [include_dir]: _include_dir
43#[macro_export]
44macro_rules! include_dir_vfs {
45    ($path:expr) => {{
46        $crate::vfs::include_dir_vfs::IncludeDirVfs::new(
47            $crate::vfs::include_dir_vfs::include_dir_patched!($path),
48        )
49    }};
50}
51
52pub use dir_structure_macros::include_dir_patched;
53
54#[derive(Debug)]
55struct NormalizeError;
56
57impl fmt::Display for NormalizeError {
58    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59        write!(f, "path normalization error")
60    }
61}
62
63impl error::Error for NormalizeError {}
64
65/// [`Path::normalize_lexically`] is unstable, so we implement a simplified version here.
66///
67/// This version also allows for paths to begin with the current directory (`.`).
68fn normalize_lexically(path: &Path) -> StdResult<PathBuf, NormalizeError> {
69    let mut lexical = PathBuf::new();
70    let mut iter = path.components().peekable();
71
72    // Find the root, if any, and add it to the lexical path.
73    // Here we treat the Windows path "C:\" as a single "root" even though
74    // `components` splits it into two: (Prefix, RootDir).
75    let root = match iter.peek() {
76        Some(Component::ParentDir) => return Err(NormalizeError),
77        Some(p @ Component::RootDir) => {
78            lexical.push(p);
79            iter.next();
80            lexical.as_os_str().len()
81        }
82        Some(Component::CurDir) => {
83            iter.next();
84            0
85        }
86        Some(Component::Prefix(prefix)) => {
87            lexical.push(prefix.as_os_str());
88            iter.next();
89            if let Some(p @ Component::RootDir) = iter.peek() {
90                lexical.push(p);
91                iter.next();
92            }
93            lexical.as_os_str().len()
94        }
95        None => return Ok(PathBuf::new()),
96        Some(Component::Normal(_)) => 0,
97    };
98
99    for component in iter {
100        match component {
101            Component::RootDir => unreachable!(),
102            Component::Prefix(_) => return Err(NormalizeError),
103            Component::CurDir => continue,
104            Component::ParentDir => {
105                // It's an error if ParentDir causes us to go above the "root".
106                if lexical.as_os_str().len() == root {
107                    return Err(NormalizeError);
108                } else {
109                    lexical.pop();
110                }
111            }
112            Component::Normal(path) => lexical.push(path),
113        }
114    }
115    Ok(lexical)
116}
117
118fn norm(path: &Path) -> Result<PathBuf, PathBuf> {
119    normalize_lexically(path).map_err(|e| {
120        Error::Io(
121            path.to_path_buf(),
122            io::Error::new(io::ErrorKind::InvalidInput, e).into(),
123        )
124    })
125}
126
127fn get_dir_or_root<'a>(root: &'a Dir<'static>, path: &Path) -> Result<&'a Dir<'static>, PathBuf> {
128    if path.as_os_str().is_empty() || path == Path::new(".") {
129        return Ok(root);
130    }
131
132    let p = norm(path)?;
133    root.get_dir(&p)
134        .ok_or(Error::Io(p, io::ErrorKind::NotFound.into()))
135}
136
137impl VfsCore for IncludeDirVfs {
138    type Path = Path;
139}
140
141impl<'vfs> Vfs<'vfs> for IncludeDirVfs {
142    type DirWalk<'a>
143        = IncludeDirWalker<'a>
144    where
145        'vfs: 'a,
146        Self: 'a;
147    type RFile = io::Cursor<&'static [u8]>;
148
149    fn open_read(self: Pin<&Self>, path: &Path) -> VfsResult<Self::RFile, Self> {
150        let p = norm(path)?;
151        if self.dir.get_dir(&p).is_some() {
152            return Err(Error::Io(p, io::ErrorKind::IsADirectory.into()));
153        }
154
155        let file = self
156            .dir
157            .get_file(&p)
158            .ok_or(Error::Io(p.clone(), io::ErrorKind::NotFound.into()))?;
159
160        Ok(io::Cursor::new(file.contents()))
161    }
162
163    fn read(self: Pin<&Self>, path: &Path) -> VfsResult<Vec<u8>, Self> {
164        let p = norm(path)?;
165        self.dir
166            .get_file(&p)
167            .map(|it| it.contents().to_vec())
168            .ok_or(Error::Io(p, io::ErrorKind::NotFound.into()))
169    }
170
171    fn read_string(self: Pin<&Self>, path: &Path) -> VfsResult<String, Self> {
172        let p = norm(path)?;
173        #[derive(Debug)]
174        struct Utf8Error;
175        impl fmt::Display for Utf8Error {
176            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
177                write!(f, "invalid utf-8: corrupt contents")
178            }
179        }
180
181        impl error::Error for Utf8Error {
182            fn description(&self) -> &str {
183                "invalid utf-8: corrupt contents"
184            }
185        }
186
187        if self.dir.get_dir(&p).is_some() {
188            return Err(Error::Io(p, io::ErrorKind::IsADirectory.into()));
189        }
190
191        let file = self
192            .dir
193            .get_file(&p)
194            .ok_or(Error::Io(p.clone(), io::ErrorKind::NotFound.into()))?;
195
196        file.contents_utf8()
197            .map(|s| s.to_string())
198            .ok_or(Error::Parse(p, Box::new(Utf8Error)))
199    }
200
201    fn exists(self: Pin<&Self>, path: &Path) -> VfsResult<bool, Self> {
202        let path = norm(path)?;
203
204        Ok(get_dir_or_root(&self.dir, &path)
205            .map_or_else(|_| self.dir.get_file(&path).is_some(), |_| true))
206    }
207
208    fn is_dir(self: Pin<&Self>, path: &Self::Path) -> VfsResult<bool, Self> {
209        let path = norm(path)?;
210        Ok(self.dir.get_dir(&path).is_some())
211    }
212
213    fn walk_dir<'b>(self: Pin<&'b Self>, path: &Path) -> VfsResult<Self::DirWalk<'b>, Self>
214    where
215        'vfs: 'b,
216    {
217        let path = norm(path)?;
218        Ok(IncludeDirWalker(
219            get_dir_or_root(&self.dir, &path)?.entries().iter(),
220        ))
221    }
222}
223
224/// The [`DirWalker`] implementation for [`IncludeDirVfs`].
225pub struct IncludeDirWalker<'a>(slice::Iter<'a, DirEntry<'static>>);
226
227impl<'a> DirWalker<'a> for IncludeDirWalker<'a> {
228    type P = Path;
229
230    fn next(&mut self) -> Option<Result<DirEntryInfo<Self::P>, <Self::P as PathType>::OwnedPath>> {
231        let next_entry = self.0.next()?;
232        match next_entry {
233            DirEntry::Dir(dir) => Some(Ok(DirEntryInfo {
234                kind: DirEntryKind::Directory,
235                name: dir.path().file_name().unwrap().to_os_string(),
236                path: dir.path().to_path_buf(),
237            })),
238            DirEntry::File(file) => Some(Ok(DirEntryInfo {
239                kind: DirEntryKind::File,
240                name: file.path().file_name().unwrap().to_os_string(),
241                path: file.path().to_path_buf(),
242            })),
243        }
244    }
245}