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 std::ffi::OsStr;
26use std::{
27    fmt::Debug,
28    fs::File,
29    future::{ready, Future},
30    io::{BufReader, Cursor, Read, Seek, Write},
31    iter::empty,
32    path::{Path, PathBuf},
33    pin::Pin,
34};
35
36/// Trait for files readers ensuring they implement the required traits
37pub trait FileReader: Debug + Send + Sync + Read + Seek + 'static {
38    /// Returns the length in bytes, if available
39    fn byte_len(&self) -> Option<u64>;
40}
41
42impl FileReader for File {
43    fn byte_len(&self) -> Option<u64> {
44        match self.metadata() {
45            Ok(metadata) => Some(metadata.len()),
46            _ => None,
47        }
48    }
49}
50
51impl<T> FileReader for Cursor<T>
52where
53    T: Debug + Send + Sync + std::convert::AsRef<[u8]> + 'static,
54{
55    fn byte_len(&self) -> Option<u64> {
56        let inner = self.get_ref();
57        Some(inner.as_ref().len().try_into().unwrap())
58    }
59}
60impl FileReader for BufReader<File> {
61    fn byte_len(&self) -> Option<u64> {
62        self.get_ref().byte_len()
63    }
64}
65
66/// Interface wrapping IO operations for doing this like loading files
67/// for resources
68pub trait ResourceIo: Send + Sync + 'static {
69    /// Attempts to load the file at the provided path returning
70    /// the entire byte contents of the file or an error
71    fn load_file<'a>(&'a self, path: &'a Path) -> ResourceIoFuture<'a, Result<Vec<u8>, FileError>>;
72
73    /// Attempts to asynchronously write the given set of bytes to the specified path.
74    fn write_file<'a>(
75        &'a self,
76        path: &'a Path,
77        data: Vec<u8>,
78    ) -> ResourceIoFuture<'a, Result<(), FileError>>;
79
80    /// Attempts to synchronously write the given set of bytes to the specified path. This method
81    /// is optional, on some platforms it may not even be supported (WebAssembly).
82    fn write_file_sync(&self, path: &Path, data: &[u8]) -> Result<(), FileError>;
83
84    /// Attempts to move a file at the given `source` path to the given `dest` path.
85    fn move_file<'a>(
86        &'a self,
87        source: &'a Path,
88        dest: &'a Path,
89    ) -> ResourceIoFuture<'a, Result<(), FileError>>;
90
91    /// Attempts to delete a file at the given `path` asynchronously.
92    fn delete_file<'a>(&'a self, path: &'a Path) -> ResourceIoFuture<'a, Result<(), FileError>>;
93
94    /// Attempts to delete a file at the given `path` synchronously.
95    fn delete_file_sync(&self, path: &Path) -> Result<(), FileError>;
96
97    /// Attempts to copy a file at the given `source` path to the given `dest` path.
98    fn copy_file<'a>(
99        &'a self,
100        source: &'a Path,
101        dest: &'a Path,
102    ) -> ResourceIoFuture<'a, Result<(), FileError>>;
103
104    /// Tries to convert the path to its canonical form (normalize it in other terms). This method
105    /// should guarantee correct behaviour for relative paths. Symlinks aren't mandatory to
106    /// follow.
107    fn canonicalize_path<'a>(
108        &'a self,
109        path: &'a Path,
110    ) -> ResourceIoFuture<'a, Result<PathBuf, FileError>> {
111        Box::pin(ready(Ok(path.to_owned())))
112    }
113
114    /// Provides an iterator over the paths present in the provided
115    /// path, this should only provide paths immediately within the directory
116    ///
117    /// Default implementation is no-op returning an empty iterator
118    fn read_directory<'a>(
119        &'a self,
120        #[allow(unused)] path: &'a Path,
121    ) -> ResourceIoFuture<'a, Result<Box<dyn Iterator<Item = PathBuf> + Send>, FileError>> {
122        let iter: Box<dyn Iterator<Item = PathBuf> + Send> = Box::new(empty());
123        Box::pin(ready(Ok(iter)))
124    }
125
126    /// Provides an iterator over the paths present in the provided
127    /// path directory this implementation should walk the directory paths
128    ///
129    /// Default implementation is no-op returning an empty iterator
130    fn walk_directory<'a>(
131        &'a self,
132        #[allow(unused)] path: &'a Path,
133        #[allow(unused)] max_depth: usize,
134    ) -> ResourceIoFuture<'a, Result<Box<dyn Iterator<Item = PathBuf> + Send>, FileError>> {
135        let iter: Box<dyn Iterator<Item = PathBuf> + Send> = Box::new(empty());
136        Box::pin(ready(Ok(iter)))
137    }
138
139    /// Attempts to open a file reader to the proivded path for
140    /// reading its bytes
141    ///
142    /// Default implementation loads the entire file contents from `load_file`
143    /// then uses a cursor as the reader
144    fn file_reader<'a>(
145        &'a self,
146        path: &'a Path,
147    ) -> ResourceIoFuture<'a, Result<Box<dyn FileReader>, FileError>> {
148        Box::pin(async move {
149            let bytes = self.load_file(path).await?;
150            let read: Box<dyn FileReader> = Box::new(Cursor::new(bytes));
151            Ok(read)
152        })
153    }
154
155    /// Checks whether the given file name is valid or not.
156    fn is_valid_file_name(&self, name: &OsStr) -> bool;
157
158    /// Used to check whether a path exists
159    fn exists<'a>(&'a self, path: &'a Path) -> ResourceIoFuture<'a, bool>;
160
161    /// Used to check whether a path is a file
162    fn is_file<'a>(&'a self, path: &'a Path) -> ResourceIoFuture<'a, bool>;
163
164    /// Used to check whether a path is a dir
165    fn is_dir<'a>(&'a self, path: &'a Path) -> ResourceIoFuture<'a, bool>;
166}
167
168/// Standard resource IO provider that uses the file system to
169/// load the file bytes
170#[derive(Default)]
171pub struct FsResourceIo;
172
173/// Future for resource io loading
174#[cfg(target_arch = "wasm32")]
175pub type ResourceIoFuture<'a, V> = Pin<Box<dyn Future<Output = V> + 'a>>;
176/// Future for resource io loading
177#[cfg(not(target_arch = "wasm32"))]
178pub type ResourceIoFuture<'a, V> = Pin<Box<dyn Future<Output = V> + Send + 'a>>;
179
180/// Iterator of paths
181#[cfg(target_arch = "wasm32")]
182pub type PathIter = Box<dyn Iterator<Item = PathBuf>>;
183/// Iterator of paths
184#[cfg(not(target_arch = "wasm32"))]
185pub type PathIter = Box<dyn Iterator<Item = PathBuf> + Send>;
186
187impl ResourceIo for FsResourceIo {
188    fn load_file<'a>(&'a self, path: &'a Path) -> ResourceIoFuture<'a, Result<Vec<u8>, FileError>> {
189        Box::pin(fyrox_core::io::load_file(path))
190    }
191
192    fn write_file<'a>(
193        &'a self,
194        path: &'a Path,
195        data: Vec<u8>,
196    ) -> ResourceIoFuture<'a, Result<(), FileError>> {
197        Box::pin(async move {
198            let mut file = File::create(path)?;
199            file.write_all(&data)?;
200            Ok(())
201        })
202    }
203
204    fn write_file_sync(&self, path: &Path, data: &[u8]) -> Result<(), FileError> {
205        let mut file = File::create(path)?;
206        file.write_all(data)?;
207        Ok(())
208    }
209
210    fn move_file<'a>(
211        &'a self,
212        source: &'a Path,
213        dest: &'a Path,
214    ) -> ResourceIoFuture<'a, Result<(), FileError>> {
215        Box::pin(async move {
216            std::fs::rename(source, dest)?;
217            Ok(())
218        })
219    }
220
221    fn delete_file<'a>(&'a self, path: &'a Path) -> ResourceIoFuture<'a, Result<(), FileError>> {
222        Box::pin(async move {
223            std::fs::remove_file(path)?;
224            Ok(())
225        })
226    }
227
228    fn delete_file_sync(&self, path: &Path) -> Result<(), FileError> {
229        std::fs::remove_file(path)?;
230        Ok(())
231    }
232
233    fn copy_file<'a>(
234        &'a self,
235        source: &'a Path,
236        dest: &'a Path,
237    ) -> ResourceIoFuture<'a, Result<(), FileError>> {
238        Box::pin(async move {
239            std::fs::copy(source, dest)?;
240            Ok(())
241        })
242    }
243
244    fn canonicalize_path<'a>(
245        &'a self,
246        path: &'a Path,
247    ) -> ResourceIoFuture<'a, Result<PathBuf, FileError>> {
248        Box::pin(async move { Ok(std::fs::canonicalize(path)?) })
249    }
250
251    /// wasm should fallback to the default no-op impl as im not sure if they
252    /// can directly read a directory
253    ///
254    /// Note: Android directory reading should be possible just I have not created
255    /// an implementation for this yet
256    #[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
257    fn read_directory<'a>(
258        &'a self,
259        #[allow(unused)] path: &'a Path,
260    ) -> ResourceIoFuture<'a, Result<PathIter, FileError>> {
261        Box::pin(async move {
262            let iter = std::fs::read_dir(path)?.flatten().map(|entry| entry.path());
263            let iter: PathIter = Box::new(iter);
264            Ok(iter)
265        })
266    }
267
268    /// Android and wasm should fallback to the default no-op impl as they cant be
269    /// walked with WalkDir
270    #[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
271    fn walk_directory<'a>(
272        &'a self,
273        path: &'a Path,
274        max_depth: usize,
275    ) -> ResourceIoFuture<'a, Result<PathIter, FileError>> {
276        Box::pin(async move {
277            use walkdir::WalkDir;
278
279            let iter = WalkDir::new(path)
280                .max_depth(max_depth)
281                .into_iter()
282                .flatten()
283                .map(|value| value.into_path());
284
285            let iter: PathIter = Box::new(iter);
286
287            Ok(iter)
288        })
289    }
290
291    /// Only use file reader when not targetting android or wasm
292    ///
293    /// Note: Might be possible to use the Android Asset struct for reading as
294    /// long as its Send + Sync + 'static (It already implements Debug + Read + Seek)
295    #[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
296    fn file_reader<'a>(
297        &'a self,
298        path: &'a Path,
299    ) -> ResourceIoFuture<'a, Result<Box<dyn FileReader>, FileError>> {
300        Box::pin(async move {
301            let file = match std::fs::File::open(path) {
302                Ok(file) => file,
303                Err(e) => return Err(FileError::Io(e)),
304            };
305
306            let read: Box<dyn FileReader> = Box::new(std::io::BufReader::new(file));
307            Ok(read)
308        })
309    }
310
311    fn is_valid_file_name(&self, name: &OsStr) -> bool {
312        for &byte in name.as_encoded_bytes() {
313            #[cfg(windows)]
314            {
315                if matches!(
316                    byte,
317                    b'<' | b'>' | b':' | b'"' | b'/' | b'\\' | b'|' | b'?' | b'*'
318                ) {
319                    return false;
320                }
321
322                // ASCII control characters
323                if byte < 32 {
324                    return false;
325                }
326            }
327
328            #[cfg(not(windows))]
329            {
330                if matches!(byte, b'0' | b'/') {
331                    return false;
332                }
333            }
334        }
335
336        true
337    }
338
339    fn exists<'a>(&'a self, path: &'a Path) -> ResourceIoFuture<'a, bool> {
340        Box::pin(fyrox_core::io::exists(path))
341    }
342
343    fn is_file<'a>(&'a self, path: &'a Path) -> ResourceIoFuture<'a, bool> {
344        Box::pin(fyrox_core::io::is_file(path))
345    }
346
347    fn is_dir<'a>(&'a self, path: &'a Path) -> ResourceIoFuture<'a, bool> {
348        Box::pin(fyrox_core::io::is_dir(path))
349    }
350}