Skip to main content

darklua_core/frontend/
resources.rs

1use std::{
2    collections::HashMap,
3    fs::{self, File},
4    io::{self, BufWriter, ErrorKind as IOErrorKind, Write},
5    iter,
6    path::{Path, PathBuf},
7    str::Utf8Error,
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, Vec<u8>>>>),
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        self.get_bytes(location).and_then(|bytes| {
56            String::from_utf8(bytes)
57                .map_err(|err| ResourceError::expected_utf8(location, err.utf8_error()))
58        })
59    }
60
61    fn get_bytes(&self, location: &Path) -> ResourceResult<Vec<u8>> {
62        match self {
63            Self::FileSystem => fs::read(location).map_err(|err| match err.kind() {
64                IOErrorKind::NotFound => ResourceError::not_found(location),
65                _ => ResourceError::io_error(location, err),
66            }),
67            Self::Memory(data) => {
68                let data = data.lock().unwrap();
69                let location = normalize_path(location);
70
71                data.get(&location)
72                    .cloned()
73                    .ok_or_else(|| ResourceError::not_found(location))
74            }
75        }
76    }
77
78    pub fn write(&self, location: &Path, content: &str) -> ResourceResult<()> {
79        self.write_bytes(location, content.as_bytes())
80    }
81
82    fn write_bytes(&self, location: &Path, content: &[u8]) -> ResourceResult<()> {
83        match self {
84            Self::FileSystem => {
85                if let Some(parent) = location.parent() {
86                    fs::create_dir_all(parent)
87                        .map_err(|err| ResourceError::io_error(parent, err))?;
88                };
89
90                let file =
91                    File::create(location).map_err(|err| ResourceError::io_error(location, err))?;
92
93                let mut file = BufWriter::new(file);
94                file.write_all(content)
95                    .map_err(|err| ResourceError::io_error(location, err))
96            }
97            Self::Memory(data) => {
98                let mut data = data.lock().unwrap();
99                data.insert(normalize_path(location), content.to_vec());
100                Ok(())
101            }
102        }
103    }
104
105    pub fn walk(&self, location: &Path) -> impl Iterator<Item = PathBuf> {
106        match self {
107            Self::FileSystem => Box::new(walk_file_system(location.to_path_buf()))
108                as Box<dyn Iterator<Item = PathBuf>>,
109            Self::Memory(data) => {
110                let data = data.lock().unwrap();
111                let location = normalize_path(location);
112                let mut paths: Vec<_> = data.keys().map(normalize_path).collect();
113                paths.retain(|path| path.starts_with(&location));
114
115                Box::new(paths.into_iter())
116            }
117        }
118    }
119
120    fn walk_all(&self, location: &Path) -> impl Iterator<Item = ResourceContent> {
121        match self {
122            Self::FileSystem => Box::new(walk_all_file_system(location.to_path_buf()))
123                as Box<dyn Iterator<Item = ResourceContent>>,
124            Self::Memory(data) => {
125                let data = data.lock().unwrap();
126                let location = normalize_path(location);
127                let mut paths: Vec<_> = data.keys().map(normalize_path).collect();
128                paths.retain(|path| path.starts_with(&location));
129
130                Box::new(paths.into_iter().map(ResourceContent::File))
131            }
132        }
133    }
134
135    fn is_empty_directory(&self, location: &Path) -> ResourceResult<bool> {
136        if !self.is_directory(location)? {
137            return Ok(false);
138        }
139
140        match self {
141            Self::FileSystem => match location.read_dir() {
142                Ok(read_dir) => {
143                    for entry in read_dir {
144                        match entry {
145                            Ok(_) => return Ok(false),
146                            Err(err) if err.kind() == io::ErrorKind::NotFound => {}
147                            Err(err) => {
148                                log::warn!(
149                                    "unable to read directory entry `{}`: {}",
150                                    location.display(),
151                                    err
152                                );
153                                return Ok(false);
154                            }
155                        }
156                    }
157
158                    Ok(true)
159                }
160                Err(err) => {
161                    log::warn!("unable to read directory `{}`: {}", location.display(), err);
162                    Ok(false)
163                }
164            },
165            Self::Memory(_data) => Ok(false),
166        }
167    }
168
169    fn remove(&self, location: &Path) -> Result<(), ResourceError> {
170        match self {
171            Self::FileSystem => {
172                if !self.exists(location)? {
173                    Ok(())
174                } else if self.is_file(location)? {
175                    fs::remove_file(location).map_err(|err| ResourceError::io_error(location, err))
176                } else if self.is_directory(location)? {
177                    fs::remove_dir_all(location)
178                        .map_err(|err| ResourceError::io_error(location, err))
179                } else {
180                    Ok(())
181                }
182            }
183            Self::Memory(data) => {
184                if self.is_file(location)? {
185                    let mut data = data.lock().unwrap();
186                    data.remove(&normalize_path(location));
187                } else if self.is_directory(location)? {
188                    let mut data = data.lock().unwrap();
189                    let location = normalize_path(location);
190                    data.retain(|path, _| !path.starts_with(&location));
191                }
192
193                Ok(())
194            }
195        }
196    }
197}
198
199fn walk_all_file_system(location: PathBuf) -> impl Iterator<Item = ResourceContent> {
200    let mut unknown_paths = vec![location];
201    let mut entries = Vec::new();
202    let mut dir_entries = Vec::new();
203
204    iter::from_fn(move || loop {
205        if let Some(location) = unknown_paths.pop() {
206            match location.metadata() {
207                Ok(metadata) => {
208                    if metadata.is_file() {
209                        entries.push(ResourceContent::File(location.to_path_buf()));
210                    } else if metadata.is_dir() {
211                        entries.push(ResourceContent::Directory(location.to_path_buf()));
212                        dir_entries.push(location.to_path_buf());
213                    } else if metadata.is_symlink() {
214                        log::warn!("unexpected symlink `{}` not followed", location.display());
215                    } else {
216                        log::warn!(
217                            concat!(
218                                "path `{}` points to an unexpected location that is not a ",
219                                "file, not a directory and not a symlink"
220                            ),
221                            location.display()
222                        );
223                    };
224                }
225                Err(err) => {
226                    log::warn!(
227                        "unable to read metadata from file `{}`: {}",
228                        location.display(),
229                        err
230                    );
231                }
232            }
233        } else if let Some(dir_location) = dir_entries.pop() {
234            match dir_location.read_dir() {
235                Ok(read_dir) => {
236                    for entry in read_dir {
237                        match entry {
238                            Ok(entry) => {
239                                unknown_paths.push(entry.path());
240                            }
241                            Err(err) => {
242                                log::warn!(
243                                    "unable to read directory entry `{}`: {}",
244                                    dir_location.display(),
245                                    err
246                                );
247                            }
248                        }
249                    }
250                }
251                Err(err) => {
252                    log::warn!(
253                        "unable to read directory `{}`: {}",
254                        dir_location.display(),
255                        err
256                    );
257                }
258            }
259        } else if let Some(path) = entries.pop() {
260            break Some(path);
261        } else {
262            break None;
263        }
264    })
265}
266
267fn walk_file_system(location: PathBuf) -> impl Iterator<Item = PathBuf> {
268    walk_all_file_system(location).filter_map(|content| match content {
269        ResourceContent::File(path) => Some(path),
270        ResourceContent::Directory(_) => None,
271    })
272}
273
274/// A resource manager for handling file operations.
275///
276/// This struct provides an abstraction over file system operations, allowing
277/// operations to be performed either on the actual file system or in memory.
278/// It handles reading, writing, and managing files and directories.
279#[derive(Debug, Clone)]
280pub struct Resources {
281    source: Source,
282}
283
284impl Resources {
285    /// Creates a new resource manager that operates on the file system.
286    pub fn from_file_system() -> Self {
287        Self {
288            source: Source::FileSystem,
289        }
290    }
291
292    /// Creates a new resource manager that operates in memory.
293    ///
294    /// This is useful for testing or when you want to process files without
295    /// writing to disk.
296    pub fn from_memory() -> Self {
297        Self {
298            source: Source::Memory(Arc::new(Mutex::new(HashMap::new()))),
299        }
300    }
301
302    /// Collects all files in the specified location. Deprecated in favor of [Self::walk].
303    #[deprecated(since = "0.19.0", note = "use `Resources::walk(location)` instead")]
304    pub fn collect_work(&self, location: impl AsRef<Path>) -> impl Iterator<Item = PathBuf> {
305        self.source.walk(location.as_ref())
306    }
307
308    /// Checks if a path exists.
309    pub fn exists(&self, location: impl AsRef<Path>) -> ResourceResult<bool> {
310        self.source.exists(location.as_ref())
311    }
312
313    /// Checks if a path is a directory.
314    pub fn is_directory(&self, location: impl AsRef<Path>) -> ResourceResult<bool> {
315        self.source.is_directory(location.as_ref())
316    }
317
318    /// Checks if a path is a file.
319    pub fn is_file(&self, location: impl AsRef<Path>) -> ResourceResult<bool> {
320        self.source.is_file(location.as_ref())
321    }
322
323    /// Reads the contents of a file.
324    pub fn get(&self, location: impl AsRef<Path>) -> ResourceResult<String> {
325        self.source.get(location.as_ref())
326    }
327
328    /// Reads the contents of a file as bytes.
329    pub fn get_bytes(&self, location: impl AsRef<Path>) -> ResourceResult<Vec<u8>> {
330        self.source.get_bytes(location.as_ref())
331    }
332
333    /// Writes content to a file.
334    pub fn write(&self, location: impl AsRef<Path>, content: &str) -> ResourceResult<()> {
335        self.source.write(location.as_ref(), content)
336    }
337
338    /// Writes content to a file as bytes.
339    pub fn write_bytes(&self, location: impl AsRef<Path>, content: &[u8]) -> ResourceResult<()> {
340        self.source.write_bytes(location.as_ref(), content)
341    }
342
343    /// Removes a file or directory.
344    pub fn remove(&self, location: impl AsRef<Path>) -> ResourceResult<()> {
345        self.source.remove(location.as_ref())
346    }
347
348    /// Walks through all files in a directory.
349    pub fn walk(&self, location: impl AsRef<Path>) -> impl Iterator<Item = PathBuf> {
350        self.source.walk(location.as_ref())
351    }
352
353    /// Walks through all files and directories in a directory.
354    pub(crate) fn walk_all(
355        &self,
356        location: impl AsRef<Path>,
357    ) -> impl Iterator<Item = ResourceContent> {
358        self.source.walk_all(location.as_ref())
359    }
360
361    pub(crate) fn is_empty_directory(&self, location: impl AsRef<Path>) -> ResourceResult<bool> {
362        self.source.is_empty_directory(location.as_ref())
363    }
364}
365
366pub(crate) enum ResourceContent {
367    File(PathBuf),
368    Directory(PathBuf),
369}
370
371/// An error that can occur during operations on [`Resource`].
372#[derive(Debug, Clone, PartialEq, Eq)]
373pub enum ResourceError {
374    /// The requested resource was not found.
375    NotFound(PathBuf),
376    /// The requested resource is expected to be valid UTF-8, but is not.
377    ExpectedUtf8 {
378        path: PathBuf,
379        utf8_error: Utf8Error,
380    },
381    /// An I/O error occurred while accessing the resource.
382    IO { path: PathBuf, error: String },
383}
384
385impl ResourceError {
386    pub(crate) fn not_found(path: impl Into<PathBuf>) -> Self {
387        Self::NotFound(path.into())
388    }
389
390    pub(crate) fn expected_utf8(path: impl Into<PathBuf>, utf8_error: Utf8Error) -> Self {
391        Self::ExpectedUtf8 {
392            path: path.into(),
393            utf8_error,
394        }
395    }
396
397    pub(crate) fn io_error(path: impl Into<PathBuf>, error: io::Error) -> Self {
398        Self::IO {
399            path: path.into(),
400            error: error.to_string(),
401        }
402    }
403}
404
405/// A type alias for `Result<T, ResourceError>`.
406type ResourceResult<T> = Result<T, ResourceError>;
407
408#[cfg(test)]
409mod test {
410    use super::*;
411
412    fn any_path() -> &'static Path {
413        Path::new("test.lua")
414    }
415
416    const ANY_CONTENT: &str = "return true";
417
418    mod memory {
419        use std::iter::FromIterator;
420
421        use super::*;
422
423        fn new() -> Resources {
424            Resources::from_memory()
425        }
426
427        #[test]
428        fn not_created_file_does_not_exist() {
429            assert_eq!(new().exists(any_path()), Ok(false));
430        }
431
432        #[test]
433        fn created_file_exists() {
434            let resources = new();
435            resources.write(any_path(), ANY_CONTENT).unwrap();
436
437            assert_eq!(resources.exists(any_path()), Ok(true));
438        }
439
440        #[test]
441        fn created_file_is_removed_exists() {
442            let resources = new();
443            resources.write(any_path(), ANY_CONTENT).unwrap();
444
445            resources.remove(any_path()).unwrap();
446
447            assert_eq!(resources.exists(any_path()), Ok(false));
448        }
449
450        #[test]
451        fn created_file_exists_is_a_file() {
452            let resources = new();
453            resources.write(any_path(), ANY_CONTENT).unwrap();
454
455            assert_eq!(resources.is_file(any_path()), Ok(true));
456        }
457
458        #[test]
459        fn created_file_exists_is_not_a_directory() {
460            let resources = new();
461            resources.write(any_path(), ANY_CONTENT).unwrap();
462
463            assert_eq!(resources.is_directory(any_path()), Ok(false));
464        }
465
466        #[test]
467        fn read_content_of_created_file() {
468            let resources = new();
469            resources.write(any_path(), ANY_CONTENT).unwrap();
470
471            assert_eq!(resources.get(any_path()), Ok(ANY_CONTENT.to_string()));
472        }
473
474        #[test]
475        fn collect_work_contains_created_files() {
476            let resources = new();
477            resources.write("src/test.lua", ANY_CONTENT).unwrap();
478
479            assert_eq!(
480                Vec::from_iter(resources.walk("src")),
481                vec![PathBuf::from("src/test.lua")]
482            );
483        }
484    }
485}