darklua_core/frontend/
resources.rs1use 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}