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#[derive(Debug, Clone)]
280pub struct Resources {
281 source: Source,
282}
283
284impl Resources {
285 pub fn from_file_system() -> Self {
287 Self {
288 source: Source::FileSystem,
289 }
290 }
291
292 pub fn from_memory() -> Self {
297 Self {
298 source: Source::Memory(Arc::new(Mutex::new(HashMap::new()))),
299 }
300 }
301
302 #[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 pub fn exists(&self, location: impl AsRef<Path>) -> ResourceResult<bool> {
310 self.source.exists(location.as_ref())
311 }
312
313 pub fn is_directory(&self, location: impl AsRef<Path>) -> ResourceResult<bool> {
315 self.source.is_directory(location.as_ref())
316 }
317
318 pub fn is_file(&self, location: impl AsRef<Path>) -> ResourceResult<bool> {
320 self.source.is_file(location.as_ref())
321 }
322
323 pub fn get(&self, location: impl AsRef<Path>) -> ResourceResult<String> {
325 self.source.get(location.as_ref())
326 }
327
328 pub fn get_bytes(&self, location: impl AsRef<Path>) -> ResourceResult<Vec<u8>> {
330 self.source.get_bytes(location.as_ref())
331 }
332
333 pub fn write(&self, location: impl AsRef<Path>, content: &str) -> ResourceResult<()> {
335 self.source.write(location.as_ref(), content)
336 }
337
338 pub fn write_bytes(&self, location: impl AsRef<Path>, content: &[u8]) -> ResourceResult<()> {
340 self.source.write_bytes(location.as_ref(), content)
341 }
342
343 pub fn remove(&self, location: impl AsRef<Path>) -> ResourceResult<()> {
345 self.source.remove(location.as_ref())
346 }
347
348 pub fn walk(&self, location: impl AsRef<Path>) -> impl Iterator<Item = PathBuf> {
350 self.source.walk(location.as_ref())
351 }
352
353 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#[derive(Debug, Clone, PartialEq, Eq)]
373pub enum ResourceError {
374 NotFound(PathBuf),
376 ExpectedUtf8 {
378 path: PathBuf,
379 utf8_error: Utf8Error,
380 },
381 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
405type 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}