1use fyrox_core::io::FileError;
25use fyrox_core::{make_relative_path, replace_slashes};
26use std::ffi::OsStr;
27use std::path::Component;
28use std::{
29 fmt::Debug,
30 fs::File,
31 future::{ready, Future},
32 io::{BufReader, Cursor, Read, Seek, Write},
33 iter::empty,
34 path::{Path, PathBuf},
35 pin::Pin,
36};
37
38pub trait FileReader: Debug + Send + Sync + Read + Seek + 'static {
40 fn byte_len(&self) -> Option<u64>;
42}
43
44impl FileReader for File {
45 fn byte_len(&self) -> Option<u64> {
46 match self.metadata() {
47 Ok(metadata) => Some(metadata.len()),
48 _ => None,
49 }
50 }
51}
52
53impl<T> FileReader for Cursor<T>
54where
55 T: Debug + Send + Sync + std::convert::AsRef<[u8]> + 'static,
56{
57 fn byte_len(&self) -> Option<u64> {
58 let inner = self.get_ref();
59 Some(inner.as_ref().len().try_into().unwrap())
60 }
61}
62impl FileReader for BufReader<File> {
63 fn byte_len(&self) -> Option<u64> {
64 self.get_ref().byte_len()
65 }
66}
67
68pub trait ResourceIo: Send + Sync + 'static {
71 fn can_write(&self) -> bool;
73 fn can_read_directories(&self) -> bool;
75 fn load_file<'a>(&'a self, path: &'a Path) -> ResourceIoFuture<'a, Result<Vec<u8>, FileError>>;
78
79 fn write_file<'a>(
81 &'a self,
82 path: &'a Path,
83 data: Vec<u8>,
84 ) -> ResourceIoFuture<'a, Result<(), FileError>>;
85
86 fn write_file_sync(&self, path: &Path, data: &[u8]) -> Result<(), FileError>;
89
90 fn create_dir_all_sync(&self, path: &Path) -> Result<(), FileError>;
92
93 fn move_file<'a>(
95 &'a self,
96 source: &'a Path,
97 dest: &'a Path,
98 ) -> ResourceIoFuture<'a, Result<(), FileError>>;
99
100 fn delete_file<'a>(&'a self, path: &'a Path) -> ResourceIoFuture<'a, Result<(), FileError>>;
102
103 fn delete_file_sync(&self, path: &Path) -> Result<(), FileError>;
105
106 fn copy_file<'a>(
108 &'a self,
109 source: &'a Path,
110 dest: &'a Path,
111 ) -> ResourceIoFuture<'a, Result<(), FileError>>;
112
113 fn canonicalize_path<'a>(&'a self, path: &'a Path) -> Result<PathBuf, FileError> {
119 Ok(path.to_owned())
120 }
121
122 fn read_directory<'a>(
127 &'a self,
128 #[allow(unused)] path: &'a Path,
129 ) -> ResourceIoFuture<'a, Result<Box<dyn Iterator<Item = PathBuf> + Send>, FileError>> {
130 let iter: Box<dyn Iterator<Item = PathBuf> + Send> = Box::new(empty());
131 Box::pin(ready(Ok(iter)))
132 }
133
134 fn walk_directory<'a>(
139 &'a self,
140 #[allow(unused)] path: &'a Path,
141 #[allow(unused)] max_depth: usize,
142 ) -> ResourceIoFuture<'a, Result<Box<dyn Iterator<Item = PathBuf> + Send>, FileError>> {
143 let iter: Box<dyn Iterator<Item = PathBuf> + Send> = Box::new(empty());
144 Box::pin(ready(Ok(iter)))
145 }
146
147 fn file_reader<'a>(
153 &'a self,
154 path: &'a Path,
155 ) -> ResourceIoFuture<'a, Result<Box<dyn FileReader>, FileError>> {
156 Box::pin(async move {
157 let bytes = self.load_file(path).await?;
158 let read: Box<dyn FileReader> = Box::new(Cursor::new(bytes));
159 Ok(read)
160 })
161 }
162
163 fn is_valid_file_name(&self, name: &OsStr) -> bool;
165
166 fn exists<'a>(&'a self, path: &'a Path) -> ResourceIoFuture<'a, bool>;
168
169 fn exists_sync(&self, path: &Path) -> bool;
171
172 fn is_file<'a>(&'a self, path: &'a Path) -> ResourceIoFuture<'a, bool>;
174
175 fn is_dir<'a>(&'a self, path: &'a Path) -> ResourceIoFuture<'a, bool>;
177}
178
179#[derive(Default)]
182pub struct FsResourceIo;
183
184#[cfg(target_arch = "wasm32")]
186pub type ResourceIoFuture<'a, V> = Pin<Box<dyn Future<Output = V> + 'a>>;
187#[cfg(not(target_arch = "wasm32"))]
189pub type ResourceIoFuture<'a, V> = Pin<Box<dyn Future<Output = V> + Send + 'a>>;
190
191#[cfg(target_arch = "wasm32")]
193pub type PathIter = Box<dyn Iterator<Item = PathBuf>>;
194#[cfg(not(target_arch = "wasm32"))]
196pub type PathIter = Box<dyn Iterator<Item = PathBuf> + Send>;
197
198pub fn normalize_path(path: impl AsRef<Path>) -> Result<PathBuf, FileError> {
206 let components = path.as_ref().components();
207 let mut ret = PathBuf::new();
208
209 for component in components {
210 match component {
211 Component::Prefix(..) | Component::RootDir => {
212 return Err(format!("Invalid path: {:?}", path.as_ref()).into());
213 }
214 Component::CurDir => {}
215 Component::ParentDir => {
216 if !ret.pop() {
217 panic!("Path may not start with ..");
218 }
219 }
220 Component::Normal(c) => {
221 ret.push(c);
222 }
223 }
224 }
225
226 if ret.as_os_str().is_empty() {
227 return Ok(".".into());
228 }
229
230 Ok(replace_slashes(ret))
233}
234
235impl ResourceIo for FsResourceIo {
236 fn can_write(&self) -> bool {
237 cfg!(all(not(target_os = "android"), not(target_arch = "wasm32")))
238 }
239 fn can_read_directories(&self) -> bool {
240 cfg!(all(not(target_os = "android"), not(target_arch = "wasm32")))
241 }
242 fn load_file<'a>(&'a self, path: &'a Path) -> ResourceIoFuture<'a, Result<Vec<u8>, FileError>> {
243 Box::pin(fyrox_core::io::load_file(path))
244 }
245
246 fn write_file<'a>(
247 &'a self,
248 path: &'a Path,
249 data: Vec<u8>,
250 ) -> ResourceIoFuture<'a, Result<(), FileError>> {
251 Box::pin(async move {
252 let mut file = File::create(path)?;
253 file.write_all(&data)?;
254 Ok(())
255 })
256 }
257
258 fn write_file_sync(&self, path: &Path, data: &[u8]) -> Result<(), FileError> {
259 let mut file = File::create(path)?;
260 file.write_all(data)?;
261 Ok(())
262 }
263
264 fn create_dir_all_sync(&self, path: &Path) -> Result<(), FileError> {
265 std::fs::create_dir_all(path)?;
266 Ok(())
267 }
268
269 fn move_file<'a>(
270 &'a self,
271 source: &'a Path,
272 dest: &'a Path,
273 ) -> ResourceIoFuture<'a, Result<(), FileError>> {
274 Box::pin(async move {
275 std::fs::rename(source, dest)?;
276 Ok(())
277 })
278 }
279
280 fn delete_file<'a>(&'a self, path: &'a Path) -> ResourceIoFuture<'a, Result<(), FileError>> {
281 Box::pin(async move {
282 std::fs::remove_file(path)?;
283 Ok(())
284 })
285 }
286
287 fn delete_file_sync(&self, path: &Path) -> Result<(), FileError> {
288 std::fs::remove_file(path)?;
289 Ok(())
290 }
291
292 fn copy_file<'a>(
293 &'a self,
294 source: &'a Path,
295 dest: &'a Path,
296 ) -> ResourceIoFuture<'a, Result<(), FileError>> {
297 Box::pin(async move {
298 std::fs::copy(source, dest)?;
299 Ok(())
300 })
301 }
302
303 fn canonicalize_path<'a>(&'a self, path: &'a Path) -> Result<PathBuf, FileError> {
304 if path.is_absolute() && self.can_read_directories() {
305 Ok(make_relative_path(path)?)
306 } else {
307 normalize_path(path)
308 }
309 }
310
311 #[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
317 fn read_directory<'a>(
318 &'a self,
319 #[allow(unused)] path: &'a Path,
320 ) -> ResourceIoFuture<'a, Result<PathIter, FileError>> {
321 Box::pin(async move {
322 let iter = std::fs::read_dir(path)?.flatten().map(|entry| entry.path());
323 let iter: PathIter = Box::new(iter);
324 Ok(iter)
325 })
326 }
327
328 #[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
331 fn walk_directory<'a>(
332 &'a self,
333 path: &'a Path,
334 max_depth: usize,
335 ) -> ResourceIoFuture<'a, Result<PathIter, FileError>> {
336 Box::pin(async move {
337 use walkdir::WalkDir;
338
339 let iter = WalkDir::new(path)
340 .max_depth(max_depth)
341 .into_iter()
342 .flatten()
343 .map(|value| value.into_path());
344
345 let iter: PathIter = Box::new(iter);
346
347 Ok(iter)
348 })
349 }
350
351 #[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
356 fn file_reader<'a>(
357 &'a self,
358 path: &'a Path,
359 ) -> ResourceIoFuture<'a, Result<Box<dyn FileReader>, FileError>> {
360 Box::pin(async move {
361 let file = match std::fs::File::open(path) {
362 Ok(file) => file,
363 Err(e) => return Err(FileError::Io(e)),
364 };
365
366 let read: Box<dyn FileReader> = Box::new(std::io::BufReader::new(file));
367 Ok(read)
368 })
369 }
370
371 fn is_valid_file_name(&self, name: &OsStr) -> bool {
372 for &byte in name.as_encoded_bytes() {
373 #[cfg(windows)]
374 {
375 if matches!(
376 byte,
377 b'<' | b'>' | b':' | b'"' | b'/' | b'\\' | b'|' | b'?' | b'*'
378 ) {
379 return false;
380 }
381
382 if byte < 32 {
384 return false;
385 }
386 }
387
388 #[cfg(not(windows))]
389 {
390 if matches!(byte, b'0' | b'/') {
391 return false;
392 }
393 }
394 }
395
396 true
397 }
398
399 fn exists<'a>(&'a self, path: &'a Path) -> ResourceIoFuture<'a, bool> {
400 Box::pin(fyrox_core::io::exists(path))
401 }
402
403 fn exists_sync(&self, path: &Path) -> bool {
404 std::fs::exists(path).unwrap_or_default()
405 }
406
407 fn is_file<'a>(&'a self, path: &'a Path) -> ResourceIoFuture<'a, bool> {
408 Box::pin(fyrox_core::io::is_file(path))
409 }
410
411 fn is_dir<'a>(&'a self, path: &'a Path) -> ResourceIoFuture<'a, bool> {
412 Box::pin(fyrox_core::io::is_dir(path))
413 }
414}
415
416#[cfg(test)]
417mod test {
418 use super::*;
419 #[cfg(target_os = "windows")]
420 #[test]
421 fn test_normalize_backslash() {
422 let path = PathBuf::from("alpha\\beta\\..\\gamma");
423 assert_eq!(normalize_path(&path).unwrap().as_os_str(), "alpha/gamma");
424 }
425 #[cfg(target_os = "windows")]
426 #[test]
427 fn test_canonicalize_backslash() {
428 let rio = FsResourceIo;
429 let path = PathBuf::from("src\\test.txt");
430 assert_eq!(
431 rio.canonicalize_path(&path).unwrap().as_os_str(),
432 "src/test.txt"
433 );
434 }
435 #[test]
436 fn test_normalize() {
437 let path = PathBuf::from("alpha/beta");
438 assert_eq!(normalize_path(&path).unwrap().as_os_str(), "alpha/beta");
439 let path = PathBuf::from("alpha/..");
440 assert_eq!(normalize_path(&path).unwrap().as_os_str(), ".");
441 }
442 #[test]
443 fn test_canonicalize() {
444 let rio = FsResourceIo;
445 let path = PathBuf::from("src/test.txt");
446 assert_eq!(
447 rio.canonicalize_path(&path).unwrap().as_os_str(),
448 "src/test.txt"
449 );
450 let path = PathBuf::from("test.txt");
451 assert_eq!(
452 rio.canonicalize_path(&path).unwrap().as_os_str(),
453 "test.txt"
454 );
455 let path = PathBuf::from(".");
456 assert_eq!(rio.canonicalize_path(&path).unwrap().as_os_str(), ".");
457 let path = PathBuf::from("src")
458 .canonicalize()
459 .unwrap()
460 .join("test.txt");
461 assert_eq!(
462 rio.canonicalize_path(&path).unwrap().as_os_str(),
463 "src/test.txt"
464 );
465 }
466}