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    fn remove(&self, location: &Path) -> Result<(), ResourceError> {
110        match self {
111            Self::FileSystem => {
112                if !self.exists(location)? {
113                    Ok(())
114                } else if self.is_file(location)? {
115                    fs::remove_file(location).map_err(|err| ResourceError::io_error(location, err))
116                } else if self.is_directory(location)? {
117                    fs::remove_dir_all(location)
118                        .map_err(|err| ResourceError::io_error(location, err))
119                } else {
120                    Ok(())
121                }
122            }
123            Self::Memory(data) => {
124                if self.is_file(location)? {
125                    let mut data = data.lock().unwrap();
126                    data.remove(&normalize_path(location));
127                } else if self.is_directory(location)? {
128                    let mut data = data.lock().unwrap();
129                    let location = normalize_path(location);
130                    data.retain(|path, _| !path.starts_with(&location));
131                }
132
133                Ok(())
134            }
135        }
136    }
137}
138
139fn walk_file_system(location: PathBuf) -> impl Iterator<Item = PathBuf> {
140    let mut unknown_paths = vec![location];
141    let mut file_paths = Vec::new();
142    let mut dir_entries = Vec::new();
143
144    iter::from_fn(move || loop {
145        if let Some(location) = unknown_paths.pop() {
146            match location.metadata() {
147                Ok(metadata) => {
148                    if metadata.is_file() {
149                        file_paths.push(location.to_path_buf());
150                    } else if metadata.is_dir() {
151                        dir_entries.push(location.to_path_buf());
152                    } else if metadata.is_symlink() {
153                        log::warn!("unexpected symlink `{}` not followed", location.display());
154                    } else {
155                        log::warn!(
156                            concat!(
157                                "path `{}` points to an unexpected location that is not a ",
158                                "file, not a directory and not a symlink"
159                            ),
160                            location.display()
161                        );
162                    };
163                }
164                Err(err) => {
165                    log::warn!(
166                        "unable to read metadata from file `{}`: {}",
167                        location.display(),
168                        err
169                    );
170                }
171            }
172        } else if let Some(dir_location) = dir_entries.pop() {
173            match dir_location.read_dir() {
174                Ok(read_dir) => {
175                    for entry in read_dir {
176                        match entry {
177                            Ok(entry) => {
178                                unknown_paths.push(entry.path());
179                            }
180                            Err(err) => {
181                                log::warn!(
182                                    "unable to read directory entry `{}`: {}",
183                                    dir_location.display(),
184                                    err
185                                );
186                            }
187                        }
188                    }
189                }
190                Err(err) => {
191                    log::warn!(
192                        "unable to read directory `{}`: {}",
193                        dir_location.display(),
194                        err
195                    );
196                }
197            }
198        } else if let Some(path) = file_paths.pop() {
199            break Some(path);
200        } else {
201            break None;
202        }
203    })
204}
205
206#[derive(Debug, Clone)]
207pub struct Resources {
208    source: Source,
209}
210
211impl Resources {
212    pub fn from_file_system() -> Self {
213        Self {
214            source: Source::FileSystem,
215        }
216    }
217
218    pub fn from_memory() -> Self {
219        Self {
220            source: Source::Memory(Default::default()),
221        }
222    }
223
224    pub fn collect_work(&self, location: impl AsRef<Path>) -> impl Iterator<Item = PathBuf> {
225        self.source.walk(location.as_ref()).filter(|path| {
226            matches!(
227                path.extension().and_then(OsStr::to_str),
228                Some("lua") | Some("luau")
229            )
230        })
231    }
232
233    pub fn exists(&self, location: impl AsRef<Path>) -> ResourceResult<bool> {
234        self.source.exists(location.as_ref())
235    }
236
237    pub fn is_directory(&self, location: impl AsRef<Path>) -> ResourceResult<bool> {
238        self.source.is_directory(location.as_ref())
239    }
240
241    pub fn is_file(&self, location: impl AsRef<Path>) -> ResourceResult<bool> {
242        self.source.is_file(location.as_ref())
243    }
244
245    pub fn get(&self, location: impl AsRef<Path>) -> ResourceResult<String> {
246        self.source.get(location.as_ref())
247    }
248
249    pub fn write(&self, location: impl AsRef<Path>, content: &str) -> ResourceResult<()> {
250        self.source.write(location.as_ref(), content)
251    }
252
253    pub fn remove(&self, location: impl AsRef<Path>) -> ResourceResult<()> {
254        self.source.remove(location.as_ref())
255    }
256
257    pub fn walk(&self, location: impl AsRef<Path>) -> impl Iterator<Item = PathBuf> {
258        self.source.walk(location.as_ref())
259    }
260}
261
262#[derive(Debug, Clone, PartialEq, Eq)]
263pub enum ResourceError {
264    NotFound(PathBuf),
265    IO { path: PathBuf, error: String },
266}
267
268impl ResourceError {
269    pub(crate) fn not_found(path: impl Into<PathBuf>) -> Self {
270        Self::NotFound(path.into())
271    }
272
273    pub(crate) fn io_error(path: impl Into<PathBuf>, error: io::Error) -> Self {
274        Self::IO {
275            path: path.into(),
276            error: error.to_string(),
277        }
278    }
279}
280
281type ResourceResult<T> = Result<T, ResourceError>;
282
283#[cfg(test)]
284mod test {
285    use super::*;
286
287    fn any_path() -> &'static Path {
288        Path::new("test.lua")
289    }
290
291    const ANY_CONTENT: &str = "return true";
292
293    mod memory {
294        use std::iter::FromIterator;
295
296        use super::*;
297
298        fn new() -> Resources {
299            Resources::from_memory()
300        }
301
302        #[test]
303        fn not_created_file_does_not_exist() {
304            assert_eq!(new().exists(any_path()), Ok(false));
305        }
306
307        #[test]
308        fn created_file_exists() {
309            let resources = new();
310            resources.write(any_path(), ANY_CONTENT).unwrap();
311
312            assert_eq!(resources.exists(any_path()), Ok(true));
313        }
314
315        #[test]
316        fn created_file_is_removed_exists() {
317            let resources = new();
318            resources.write(any_path(), ANY_CONTENT).unwrap();
319
320            resources.remove(any_path()).unwrap();
321
322            assert_eq!(resources.exists(any_path()), Ok(false));
323        }
324
325        #[test]
326        fn created_file_exists_is_a_file() {
327            let resources = new();
328            resources.write(any_path(), ANY_CONTENT).unwrap();
329
330            assert_eq!(resources.is_file(any_path()), Ok(true));
331        }
332
333        #[test]
334        fn created_file_exists_is_not_a_directory() {
335            let resources = new();
336            resources.write(any_path(), ANY_CONTENT).unwrap();
337
338            assert_eq!(resources.is_directory(any_path()), Ok(false));
339        }
340
341        #[test]
342        fn read_content_of_created_file() {
343            let resources = new();
344            resources.write(any_path(), ANY_CONTENT).unwrap();
345
346            assert_eq!(resources.get(any_path()), Ok(ANY_CONTENT.to_string()));
347        }
348
349        #[test]
350        fn collect_work_contains_created_files() {
351            let resources = new();
352            resources.write("src/test.lua", ANY_CONTENT).unwrap();
353
354            assert_eq!(
355                Vec::from_iter(resources.collect_work("src")),
356                vec![PathBuf::from("src/test.lua")]
357            );
358        }
359    }
360}