Skip to main content

fyrox_resource/
io.rs

1// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy
4// of this software and associated documentation files (the "Software"), to deal
5// in the Software without restriction, including without limitation the rights
6// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7// copies of the Software, and to permit persons to whom the Software is
8// furnished to do so, subject to the following conditions:
9//
10// The above copyright notice and this permission notice shall be included in all
11// copies or substantial portions of the Software.
12//
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19// SOFTWARE.
20
21//! Provides an interface for IO operations that a resource loader will use, this facilitates
22//! things such as loading assets within archive files
23
24use 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
38/// Trait for files readers ensuring they implement the required traits
39pub trait FileReader: Debug + Send + Sync + Read + Seek + 'static {
40    /// Returns the length in bytes, if available
41    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
68/// Interface wrapping IO operations for doing this like loading files
69/// for resources
70pub trait ResourceIo: Send + Sync + 'static {
71    /// True if writing to files is possible through this object.
72    fn can_write(&self) -> bool;
73    /// True if reading the content of directories is possible through this object.
74    fn can_read_directories(&self) -> bool;
75    /// Attempts to load the file at the provided path returning
76    /// the entire byte contents of the file or an error
77    fn load_file<'a>(&'a self, path: &'a Path) -> ResourceIoFuture<'a, Result<Vec<u8>, FileError>>;
78
79    /// Attempts to asynchronously write the given set of bytes to the specified path.
80    fn write_file<'a>(
81        &'a self,
82        path: &'a Path,
83        data: Vec<u8>,
84    ) -> ResourceIoFuture<'a, Result<(), FileError>>;
85
86    /// Attempts to synchronously write the given set of bytes to the specified path. This method
87    /// is optional, on some platforms it may not even be supported (WebAssembly).
88    fn write_file_sync(&self, path: &Path, data: &[u8]) -> Result<(), FileError>;
89
90    /// Creates a directory with all subdirectories defined by the specified path.
91    fn create_dir_all_sync(&self, path: &Path) -> Result<(), FileError>;
92
93    /// Attempts to move a file at the given `source` path to the given `dest` path.
94    fn move_file<'a>(
95        &'a self,
96        source: &'a Path,
97        dest: &'a Path,
98    ) -> ResourceIoFuture<'a, Result<(), FileError>>;
99
100    /// Attempts to delete a file at the given `path` asynchronously.
101    fn delete_file<'a>(&'a self, path: &'a Path) -> ResourceIoFuture<'a, Result<(), FileError>>;
102
103    /// Attempts to delete a file at the given `path` synchronously.
104    fn delete_file_sync(&self, path: &Path) -> Result<(), FileError>;
105
106    /// Attempts to copy a file at the given `source` path to the given `dest` path.
107    fn copy_file<'a>(
108        &'a self,
109        source: &'a Path,
110        dest: &'a Path,
111    ) -> ResourceIoFuture<'a, Result<(), FileError>>;
112
113    /// Tries to convert the path to its canonical form (normalize it in other terms),
114    /// and put the path into a form that is suited for use in methods of this object.
115    /// Each file should have exactly one canonical path.
116    /// This method should guarantee correct behaviour for relative paths. Symlinks aren't mandatory to
117    /// follow.
118    fn canonicalize_path<'a>(&'a self, path: &'a Path) -> Result<PathBuf, FileError> {
119        Ok(path.to_owned())
120    }
121
122    /// Provides an iterator over the paths present in the provided
123    /// path, this should only provide paths immediately within the directory
124    ///
125    /// Default implementation is no-op returning an empty iterator
126    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    /// Provides an iterator over the paths present in the provided
135    /// path directory this implementation should walk the directory paths
136    ///
137    /// Default implementation is no-op returning an empty iterator
138    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    /// Attempts to open a file reader to the proivded path for
148    /// reading its bytes
149    ///
150    /// Default implementation loads the entire file contents from `load_file`
151    /// then uses a cursor as the reader
152    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    /// Checks whether the given file name is valid or not.
164    fn is_valid_file_name(&self, name: &OsStr) -> bool;
165
166    /// Used to check whether a path exists
167    fn exists<'a>(&'a self, path: &'a Path) -> ResourceIoFuture<'a, bool>;
168
169    /// Used to check whether a path exists
170    fn exists_sync(&self, path: &Path) -> bool;
171
172    /// Used to check whether a path is a file
173    fn is_file<'a>(&'a self, path: &'a Path) -> ResourceIoFuture<'a, bool>;
174
175    /// Used to check whether a path is a dir
176    fn is_dir<'a>(&'a self, path: &'a Path) -> ResourceIoFuture<'a, bool>;
177}
178
179/// Standard resource IO provider that uses the file system to
180/// load the file bytes
181#[derive(Default)]
182pub struct FsResourceIo;
183
184/// Future for resource io loading
185#[cfg(target_arch = "wasm32")]
186pub type ResourceIoFuture<'a, V> = Pin<Box<dyn Future<Output = V> + 'a>>;
187/// Future for resource io loading
188#[cfg(not(target_arch = "wasm32"))]
189pub type ResourceIoFuture<'a, V> = Pin<Box<dyn Future<Output = V> + Send + 'a>>;
190
191/// Iterator of paths
192#[cfg(target_arch = "wasm32")]
193pub type PathIter = Box<dyn Iterator<Item = PathBuf>>;
194/// Iterator of paths
195#[cfg(not(target_arch = "wasm32"))]
196pub type PathIter = Box<dyn Iterator<Item = PathBuf> + Send>;
197
198/// Remove . and .. directories from a resource path, without accessing the file system,
199/// and replace \ with /. There is no requirement that any part of the path actually exists.
200/// The path "." is returned if the resulting path would otherwise be empty.
201///
202/// Because the file system is not accessed, all paths must be relative to the project root,
203/// and this function will return an error if the path tries to go outside of it, such as by .. directories
204/// or by being an absolute path.
205pub 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    // The resource registry uses normalized paths with `/` slashes, and this step is needed
231    // mostly on Windows which uses `\` slashes.
232    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    /// wasm should fallback to the default no-op impl as im not sure if they
312    /// can directly read a directory
313    ///
314    /// Note: Android directory reading should be possible just I have not created
315    /// an implementation for this yet
316    #[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    /// Android and wasm should fallback to the default no-op impl as they cant be
329    /// walked with WalkDir
330    #[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    /// Only use file reader when not targetting android or wasm
352    ///
353    /// Note: Might be possible to use the Android Asset struct for reading as
354    /// long as its Send + Sync + 'static (It already implements Debug + Read + Seek)
355    #[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                // ASCII control characters
383                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}