dir_structure_include_dir_vfs/
lib.rs

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