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}