darklua_core/frontend/
resources.rs

1use std::{
2    collections::HashMap,
3    ffi::OsStr,
4    fs::{self, File},
5    io::{self, BufWriter, ErrorKind as IOErrorKind, Write},
6    iter,
7    path::{Path, PathBuf},
8    sync::{Arc, Mutex},
9};
10
11use crate::utils::normalize_path;
12
13#[derive(Debug, Clone)]
14enum Source {
15    FileSystem,
16    Memory(Arc<Mutex<HashMap<PathBuf, String>>>),
17}
18
19impl Source {
20    pub fn exists(&self, location: &Path) -> ResourceResult<bool> {
21        match self {
22            Self::FileSystem => Ok(location.exists()),
23            Self::Memory(data) => Ok(data.lock().unwrap().contains_key(&normalize_path(location))),
24        }
25    }
26
27    pub fn is_directory(&self, location: &Path) -> ResourceResult<bool> {
28        let is_directory = match self {
29            Source::FileSystem => self.exists(location)? && location.is_dir(),
30            Source::Memory(data) => {
31                let data = data.lock().unwrap();
32                let location = normalize_path(location);
33
34                data.iter()
35                    .any(|(path, _content)| path != &location && path.starts_with(&location))
36            }
37        };
38        Ok(is_directory)
39    }
40
41    pub fn is_file(&self, location: &Path) -> ResourceResult<bool> {
42        let is_file = match self {
43            Source::FileSystem => self.exists(location)? && location.is_file(),
44            Source::Memory(data) => {
45                let data = data.lock().unwrap();
46                let location = normalize_path(location);
47
48                data.contains_key(&location)
49            }
50        };
51        Ok(is_file)
52    }
53
54    pub fn get(&self, location: &Path) -> ResourceResult<String> {
55        match self {
56            Self::FileSystem => fs::read_to_string(location).map_err(|err| match err.kind() {
57                IOErrorKind::NotFound => ResourceError::not_found(location),
58                _ => ResourceError::io_error(location, err),
59            }),
60            Self::Memory(data) => {
61                let data = data.lock().unwrap();
62                let location = normalize_path(location);
63
64                data.get(&location)
65                    .map(String::from)
66                    .ok_or_else(|| ResourceError::not_found(location))
67            }
68        }
69    }
70
71    pub fn write(&self, location: &Path, content: &str) -> ResourceResult<()> {
72        match self {
73            Self::FileSystem => {
74                if let Some(parent) = location.parent() {
75                    fs::create_dir_all(parent)
76                        .map_err(|err| ResourceError::io_error(parent, err))?;
77                };
78
79                let file =
80                    File::create(location).map_err(|err| ResourceError::io_error(location, err))?;
81
82                let mut file = BufWriter::new(file);
83                file.write_all(content.as_bytes())
84                    .map_err(|err| ResourceError::io_error(location, err))
85            }
86            Self::Memory(data) => {
87                let mut data = data.lock().unwrap();
88                data.insert(normalize_path(location), content.to_string());
89                Ok(())
90            }
91        }
92    }
93
94    pub fn walk(&self, location: &Path) -> impl Iterator<Item = PathBuf> {
95        match self {
96            Self::FileSystem => Box::new(walk_file_system(location.to_path_buf()))
97                as Box<dyn Iterator<Item = PathBuf>>,
98            Self::Memory(data) => {
99                let data = data.lock().unwrap();
100                let location = normalize_path(location);
101                let mut paths: Vec<_> = data.keys().map(normalize_path).collect();
102                paths.retain(|path| path.starts_with(&location));
103
104                Box::new(paths.into_iter())
105            }
106        }
107    }
108}
109
110fn walk_file_system(location: PathBuf) -> impl Iterator<Item = PathBuf> {
111    let mut unknown_paths = vec![location];
112    let mut file_paths = Vec::new();
113    let mut dir_entries = Vec::new();
114
115    iter::from_fn(move || loop {
116        if let Some(location) = unknown_paths.pop() {
117            match location.metadata() {
118                Ok(metadata) => {
119                    if metadata.is_file() {
120                        file_paths.push(location.to_path_buf());
121                    } else if metadata.is_dir() {
122                        dir_entries.push(location.to_path_buf());
123                    } else if metadata.is_symlink() {
124                        log::warn!("unexpected symlink `{}` not followed", location.display());
125                    } else {
126                        log::warn!(
127                            concat!(
128                                "path `{}` points to an unexpected location that is not a ",
129                                "file, not a directory and not a symlink"
130                            ),
131                            location.display()
132                        );
133                    };
134                }
135                Err(err) => {
136                    log::warn!(
137                        "unable to read metadata from file `{}`: {}",
138                        location.display(),
139                        err
140                    );
141                }
142            }
143        } else if let Some(dir_location) = dir_entries.pop() {
144            match dir_location.read_dir() {
145                Ok(read_dir) => {
146                    for entry in read_dir {
147                        match entry {
148                            Ok(entry) => {
149                                unknown_paths.push(entry.path());
150                            }
151                            Err(err) => {
152                                log::warn!(
153                                    "unable to read directory entry `{}`: {}",
154                                    dir_location.display(),
155                                    err
156                                );
157                            }
158                        }
159                    }
160                }
161                Err(err) => {
162                    log::warn!(
163                        "unable to read directory `{}`: {}",
164                        dir_location.display(),
165                        err
166                    );
167                }
168            }
169        } else if let Some(path) = file_paths.pop() {
170            break Some(path);
171        } else {
172            break None;
173        }
174    })
175}
176
177#[derive(Debug, Clone)]
178pub struct Resources {
179    source: Source,
180}
181
182impl Resources {
183    pub fn from_file_system() -> Self {
184        Self {
185            source: Source::FileSystem,
186        }
187    }
188
189    pub fn from_memory() -> Self {
190        Self {
191            source: Source::Memory(Default::default()),
192        }
193    }
194
195    pub fn collect_work(&self, location: impl AsRef<Path>) -> impl Iterator<Item = PathBuf> {
196        self.source.walk(location.as_ref()).filter(|path| {
197            matches!(
198                path.extension().and_then(OsStr::to_str),
199                Some("lua") | Some("luau")
200            )
201        })
202    }
203
204    pub fn exists(&self, location: impl AsRef<Path>) -> ResourceResult<bool> {
205        self.source.exists(location.as_ref())
206    }
207
208    pub fn is_directory(&self, location: impl AsRef<Path>) -> ResourceResult<bool> {
209        self.source.is_directory(location.as_ref())
210    }
211
212    pub fn is_file(&self, location: impl AsRef<Path>) -> ResourceResult<bool> {
213        self.source.is_file(location.as_ref())
214    }
215
216    pub fn get(&self, location: impl AsRef<Path>) -> ResourceResult<String> {
217        self.source.get(location.as_ref())
218    }
219
220    pub fn write(&self, location: impl AsRef<Path>, content: &str) -> ResourceResult<()> {
221        self.source.write(location.as_ref(), content)
222    }
223}
224
225#[derive(Debug, Clone, PartialEq, Eq)]
226pub enum ResourceError {
227    NotFound(PathBuf),
228    IO { path: PathBuf, error: String },
229}
230
231impl ResourceError {
232    pub(crate) fn not_found(path: impl Into<PathBuf>) -> Self {
233        Self::NotFound(path.into())
234    }
235
236    pub(crate) fn io_error(path: impl Into<PathBuf>, error: io::Error) -> Self {
237        Self::IO {
238            path: path.into(),
239            error: error.to_string(),
240        }
241    }
242}
243
244type ResourceResult<T> = Result<T, ResourceError>;
245
246#[cfg(test)]
247mod test {
248    use super::*;
249
250    fn any_path() -> &'static Path {
251        Path::new("test.lua")
252    }
253
254    const ANY_CONTENT: &str = "return true";
255
256    mod memory {
257        use std::iter::FromIterator;
258
259        use super::*;
260
261        fn new() -> Resources {
262            Resources::from_memory()
263        }
264
265        #[test]
266        fn not_created_file_does_not_exist() {
267            assert_eq!(new().exists(any_path()), Ok(false));
268        }
269
270        #[test]
271        fn created_file_exists() {
272            let resources = new();
273            resources.write(any_path(), ANY_CONTENT).unwrap();
274
275            assert_eq!(resources.exists(any_path()), Ok(true));
276        }
277
278        #[test]
279        fn created_file_exists_is_a_file() {
280            let resources = new();
281            resources.write(any_path(), ANY_CONTENT).unwrap();
282
283            assert_eq!(resources.is_file(any_path()), Ok(true));
284        }
285
286        #[test]
287        fn created_file_exists_is_not_a_directory() {
288            let resources = new();
289            resources.write(any_path(), ANY_CONTENT).unwrap();
290
291            assert_eq!(resources.is_directory(any_path()), Ok(false));
292        }
293
294        #[test]
295        fn read_content_of_created_file() {
296            let resources = new();
297            resources.write(any_path(), ANY_CONTENT).unwrap();
298
299            assert_eq!(resources.get(any_path()), Ok(ANY_CONTENT.to_string()));
300        }
301
302        #[test]
303        fn collect_work_contains_created_files() {
304            let resources = new();
305            resources.write("src/test.lua", ANY_CONTENT).unwrap();
306
307            assert_eq!(
308                Vec::from_iter(resources.collect_work("src")),
309                vec![PathBuf::from("src/test.lua")]
310            );
311        }
312    }
313}