bevy_asset/io/
mod.rs

1#[cfg(all(feature = "file_watcher", target_arch = "wasm32"))]
2compile_error!(
3    "The \"file_watcher\" feature for hot reloading does not work \
4    on WASM.\nDisable \"file_watcher\" \
5    when compiling to WASM"
6);
7
8#[cfg(target_os = "android")]
9pub mod android;
10pub mod embedded;
11#[cfg(not(target_arch = "wasm32"))]
12pub mod file;
13pub mod gated;
14pub mod memory;
15pub mod processor_gated;
16#[cfg(target_arch = "wasm32")]
17pub mod wasm;
18
19mod source;
20
21pub use futures_lite::{AsyncReadExt, AsyncWriteExt};
22pub use source::*;
23
24use bevy_utils::{BoxedFuture, ConditionalSendFuture};
25use futures_io::{AsyncRead, AsyncSeek, AsyncWrite};
26use futures_lite::{ready, Stream};
27use std::io::SeekFrom;
28use std::task::Context;
29use std::{
30    path::{Path, PathBuf},
31    pin::Pin,
32    sync::Arc,
33    task::Poll,
34};
35use thiserror::Error;
36
37/// Errors that occur while loading assets.
38#[derive(Error, Debug, Clone)]
39pub enum AssetReaderError {
40    /// Path not found.
41    #[error("Path not found: {0}")]
42    NotFound(PathBuf),
43
44    /// Encountered an I/O error while loading an asset.
45    #[error("Encountered an I/O error while loading asset: {0}")]
46    Io(Arc<std::io::Error>),
47
48    /// The HTTP request completed but returned an unhandled [HTTP response status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status).
49    /// If the request fails before getting a status code (e.g. request timeout, interrupted connection, etc), expect [`AssetReaderError::Io`].
50    #[error("Encountered HTTP status {0:?} when loading asset")]
51    HttpError(u16),
52}
53
54impl PartialEq for AssetReaderError {
55    /// Equality comparison for `AssetReaderError::Io` is not full (only through `ErrorKind` of inner error)
56    #[inline]
57    fn eq(&self, other: &Self) -> bool {
58        match (self, other) {
59            (Self::NotFound(path), Self::NotFound(other_path)) => path == other_path,
60            (Self::Io(error), Self::Io(other_error)) => error.kind() == other_error.kind(),
61            (Self::HttpError(code), Self::HttpError(other_code)) => code == other_code,
62            _ => false,
63        }
64    }
65}
66
67impl Eq for AssetReaderError {}
68
69impl From<std::io::Error> for AssetReaderError {
70    fn from(value: std::io::Error) -> Self {
71        Self::Io(Arc::new(value))
72    }
73}
74
75pub trait AsyncReadAndSeek: AsyncRead + AsyncSeek {}
76
77impl<T: AsyncRead + AsyncSeek> AsyncReadAndSeek for T {}
78
79pub type Reader<'a> = dyn AsyncReadAndSeek + Unpin + Send + Sync + 'a;
80
81/// Performs read operations on an asset storage. [`AssetReader`] exposes a "virtual filesystem"
82/// API, where asset bytes and asset metadata bytes are both stored and accessible for a given
83/// `path`. This trait is not object safe, if needed use a dyn [`ErasedAssetReader`] instead.
84///
85/// Also see [`AssetWriter`].
86pub trait AssetReader: Send + Sync + 'static {
87    /// Returns a future to load the full file data at the provided path.
88    fn read<'a>(
89        &'a self,
90        path: &'a Path,
91    ) -> impl ConditionalSendFuture<Output = Result<Box<Reader<'a>>, AssetReaderError>>;
92    /// Returns a future to load the full file data at the provided path.
93    fn read_meta<'a>(
94        &'a self,
95        path: &'a Path,
96    ) -> impl ConditionalSendFuture<Output = Result<Box<Reader<'a>>, AssetReaderError>>;
97    /// Returns an iterator of directory entry names at the provided path.
98    fn read_directory<'a>(
99        &'a self,
100        path: &'a Path,
101    ) -> impl ConditionalSendFuture<Output = Result<Box<PathStream>, AssetReaderError>>;
102    /// Returns true if the provided path points to a directory.
103    fn is_directory<'a>(
104        &'a self,
105        path: &'a Path,
106    ) -> impl ConditionalSendFuture<Output = Result<bool, AssetReaderError>>;
107    /// Reads asset metadata bytes at the given `path` into a [`Vec<u8>`]. This is a convenience
108    /// function that wraps [`AssetReader::read_meta`] by default.
109    fn read_meta_bytes<'a>(
110        &'a self,
111        path: &'a Path,
112    ) -> impl ConditionalSendFuture<Output = Result<Vec<u8>, AssetReaderError>> {
113        async {
114            let mut meta_reader = self.read_meta(path).await?;
115            let mut meta_bytes = Vec::new();
116            meta_reader.read_to_end(&mut meta_bytes).await?;
117            Ok(meta_bytes)
118        }
119    }
120}
121
122/// Equivalent to an [`AssetReader`] but using boxed futures, necessary eg. when using a `dyn AssetReader`,
123/// as [`AssetReader`] isn't currently object safe.
124pub trait ErasedAssetReader: Send + Sync + 'static {
125    /// Returns a future to load the full file data at the provided path.
126    fn read<'a>(&'a self, path: &'a Path)
127        -> BoxedFuture<Result<Box<Reader<'a>>, AssetReaderError>>;
128    /// Returns a future to load the full file data at the provided path.
129    fn read_meta<'a>(
130        &'a self,
131        path: &'a Path,
132    ) -> BoxedFuture<Result<Box<Reader<'a>>, AssetReaderError>>;
133    /// Returns an iterator of directory entry names at the provided path.
134    fn read_directory<'a>(
135        &'a self,
136        path: &'a Path,
137    ) -> BoxedFuture<Result<Box<PathStream>, AssetReaderError>>;
138    /// Returns true if the provided path points to a directory.
139    fn is_directory<'a>(&'a self, path: &'a Path) -> BoxedFuture<Result<bool, AssetReaderError>>;
140    /// Reads asset metadata bytes at the given `path` into a [`Vec<u8>`]. This is a convenience
141    /// function that wraps [`ErasedAssetReader::read_meta`] by default.
142    fn read_meta_bytes<'a>(
143        &'a self,
144        path: &'a Path,
145    ) -> BoxedFuture<Result<Vec<u8>, AssetReaderError>>;
146}
147
148impl<T: AssetReader> ErasedAssetReader for T {
149    fn read<'a>(
150        &'a self,
151        path: &'a Path,
152    ) -> BoxedFuture<Result<Box<Reader<'a>>, AssetReaderError>> {
153        Box::pin(Self::read(self, path))
154    }
155    fn read_meta<'a>(
156        &'a self,
157        path: &'a Path,
158    ) -> BoxedFuture<Result<Box<Reader<'a>>, AssetReaderError>> {
159        Box::pin(Self::read_meta(self, path))
160    }
161    fn read_directory<'a>(
162        &'a self,
163        path: &'a Path,
164    ) -> BoxedFuture<Result<Box<PathStream>, AssetReaderError>> {
165        Box::pin(Self::read_directory(self, path))
166    }
167    fn is_directory<'a>(&'a self, path: &'a Path) -> BoxedFuture<Result<bool, AssetReaderError>> {
168        Box::pin(Self::is_directory(self, path))
169    }
170    fn read_meta_bytes<'a>(
171        &'a self,
172        path: &'a Path,
173    ) -> BoxedFuture<Result<Vec<u8>, AssetReaderError>> {
174        Box::pin(Self::read_meta_bytes(self, path))
175    }
176}
177
178pub type Writer = dyn AsyncWrite + Unpin + Send + Sync;
179
180pub type PathStream = dyn Stream<Item = PathBuf> + Unpin + Send;
181
182/// Errors that occur while loading assets.
183#[derive(Error, Debug)]
184pub enum AssetWriterError {
185    /// Encountered an I/O error while loading an asset.
186    #[error("encountered an io error while loading asset: {0}")]
187    Io(#[from] std::io::Error),
188}
189
190/// Preforms write operations on an asset storage. [`AssetWriter`] exposes a "virtual filesystem"
191/// API, where asset bytes and asset metadata bytes are both stored and accessible for a given
192/// `path`. This trait is not object safe, if needed use a dyn [`ErasedAssetWriter`] instead.
193///
194/// Also see [`AssetReader`].
195pub trait AssetWriter: Send + Sync + 'static {
196    /// Writes the full asset bytes at the provided path.
197    fn write<'a>(
198        &'a self,
199        path: &'a Path,
200    ) -> impl ConditionalSendFuture<Output = Result<Box<Writer>, AssetWriterError>>;
201    /// Writes the full asset meta bytes at the provided path.
202    /// This _should not_ include storage specific extensions like `.meta`.
203    fn write_meta<'a>(
204        &'a self,
205        path: &'a Path,
206    ) -> impl ConditionalSendFuture<Output = Result<Box<Writer>, AssetWriterError>>;
207    /// Removes the asset stored at the given path.
208    fn remove<'a>(
209        &'a self,
210        path: &'a Path,
211    ) -> impl ConditionalSendFuture<Output = Result<(), AssetWriterError>>;
212    /// Removes the asset meta stored at the given path.
213    /// This _should not_ include storage specific extensions like `.meta`.
214    fn remove_meta<'a>(
215        &'a self,
216        path: &'a Path,
217    ) -> impl ConditionalSendFuture<Output = Result<(), AssetWriterError>>;
218    /// Renames the asset at `old_path` to `new_path`
219    fn rename<'a>(
220        &'a self,
221        old_path: &'a Path,
222        new_path: &'a Path,
223    ) -> impl ConditionalSendFuture<Output = Result<(), AssetWriterError>>;
224    /// Renames the asset meta for the asset at `old_path` to `new_path`.
225    /// This _should not_ include storage specific extensions like `.meta`.
226    fn rename_meta<'a>(
227        &'a self,
228        old_path: &'a Path,
229        new_path: &'a Path,
230    ) -> impl ConditionalSendFuture<Output = Result<(), AssetWriterError>>;
231    /// Removes the directory at the given path, including all assets _and_ directories in that directory.
232    fn remove_directory<'a>(
233        &'a self,
234        path: &'a Path,
235    ) -> impl ConditionalSendFuture<Output = Result<(), AssetWriterError>>;
236    /// Removes the directory at the given path, but only if it is completely empty. This will return an error if the
237    /// directory is not empty.
238    fn remove_empty_directory<'a>(
239        &'a self,
240        path: &'a Path,
241    ) -> impl ConditionalSendFuture<Output = Result<(), AssetWriterError>>;
242    /// Removes all assets (and directories) in this directory, resulting in an empty directory.
243    fn remove_assets_in_directory<'a>(
244        &'a self,
245        path: &'a Path,
246    ) -> impl ConditionalSendFuture<Output = Result<(), AssetWriterError>>;
247    /// Writes the asset `bytes` to the given `path`.
248    fn write_bytes<'a>(
249        &'a self,
250        path: &'a Path,
251        bytes: &'a [u8],
252    ) -> impl ConditionalSendFuture<Output = Result<(), AssetWriterError>> {
253        async {
254            let mut writer = self.write(path).await?;
255            writer.write_all(bytes).await?;
256            writer.flush().await?;
257            Ok(())
258        }
259    }
260    /// Writes the asset meta `bytes` to the given `path`.
261    fn write_meta_bytes<'a>(
262        &'a self,
263        path: &'a Path,
264        bytes: &'a [u8],
265    ) -> impl ConditionalSendFuture<Output = Result<(), AssetWriterError>> {
266        async {
267            let mut meta_writer = self.write_meta(path).await?;
268            meta_writer.write_all(bytes).await?;
269            meta_writer.flush().await?;
270            Ok(())
271        }
272    }
273}
274
275/// Equivalent to an [`AssetWriter`] but using boxed futures, necessary eg. when using a `dyn AssetWriter`,
276/// as [`AssetWriter`] isn't currently object safe.
277pub trait ErasedAssetWriter: Send + Sync + 'static {
278    /// Writes the full asset bytes at the provided path.
279    fn write<'a>(&'a self, path: &'a Path) -> BoxedFuture<Result<Box<Writer>, AssetWriterError>>;
280    /// Writes the full asset meta bytes at the provided path.
281    /// This _should not_ include storage specific extensions like `.meta`.
282    fn write_meta<'a>(
283        &'a self,
284        path: &'a Path,
285    ) -> BoxedFuture<Result<Box<Writer>, AssetWriterError>>;
286    /// Removes the asset stored at the given path.
287    fn remove<'a>(&'a self, path: &'a Path) -> BoxedFuture<Result<(), AssetWriterError>>;
288    /// Removes the asset meta stored at the given path.
289    /// This _should not_ include storage specific extensions like `.meta`.
290    fn remove_meta<'a>(&'a self, path: &'a Path) -> BoxedFuture<Result<(), AssetWriterError>>;
291    /// Renames the asset at `old_path` to `new_path`
292    fn rename<'a>(
293        &'a self,
294        old_path: &'a Path,
295        new_path: &'a Path,
296    ) -> BoxedFuture<Result<(), AssetWriterError>>;
297    /// Renames the asset meta for the asset at `old_path` to `new_path`.
298    /// This _should not_ include storage specific extensions like `.meta`.
299    fn rename_meta<'a>(
300        &'a self,
301        old_path: &'a Path,
302        new_path: &'a Path,
303    ) -> BoxedFuture<Result<(), AssetWriterError>>;
304    /// Removes the directory at the given path, including all assets _and_ directories in that directory.
305    fn remove_directory<'a>(&'a self, path: &'a Path) -> BoxedFuture<Result<(), AssetWriterError>>;
306    /// Removes the directory at the given path, but only if it is completely empty. This will return an error if the
307    /// directory is not empty.
308    fn remove_empty_directory<'a>(
309        &'a self,
310        path: &'a Path,
311    ) -> BoxedFuture<Result<(), AssetWriterError>>;
312    /// Removes all assets (and directories) in this directory, resulting in an empty directory.
313    fn remove_assets_in_directory<'a>(
314        &'a self,
315        path: &'a Path,
316    ) -> BoxedFuture<Result<(), AssetWriterError>>;
317    /// Writes the asset `bytes` to the given `path`.
318    fn write_bytes<'a>(
319        &'a self,
320        path: &'a Path,
321        bytes: &'a [u8],
322    ) -> BoxedFuture<Result<(), AssetWriterError>>;
323    /// Writes the asset meta `bytes` to the given `path`.
324    fn write_meta_bytes<'a>(
325        &'a self,
326        path: &'a Path,
327        bytes: &'a [u8],
328    ) -> BoxedFuture<Result<(), AssetWriterError>>;
329}
330
331impl<T: AssetWriter> ErasedAssetWriter for T {
332    fn write<'a>(&'a self, path: &'a Path) -> BoxedFuture<Result<Box<Writer>, AssetWriterError>> {
333        Box::pin(Self::write(self, path))
334    }
335    fn write_meta<'a>(
336        &'a self,
337        path: &'a Path,
338    ) -> BoxedFuture<Result<Box<Writer>, AssetWriterError>> {
339        Box::pin(Self::write_meta(self, path))
340    }
341    fn remove<'a>(&'a self, path: &'a Path) -> BoxedFuture<Result<(), AssetWriterError>> {
342        Box::pin(Self::remove(self, path))
343    }
344    fn remove_meta<'a>(&'a self, path: &'a Path) -> BoxedFuture<Result<(), AssetWriterError>> {
345        Box::pin(Self::remove_meta(self, path))
346    }
347    fn rename<'a>(
348        &'a self,
349        old_path: &'a Path,
350        new_path: &'a Path,
351    ) -> BoxedFuture<Result<(), AssetWriterError>> {
352        Box::pin(Self::rename(self, old_path, new_path))
353    }
354    fn rename_meta<'a>(
355        &'a self,
356        old_path: &'a Path,
357        new_path: &'a Path,
358    ) -> BoxedFuture<Result<(), AssetWriterError>> {
359        Box::pin(Self::rename_meta(self, old_path, new_path))
360    }
361    fn remove_directory<'a>(&'a self, path: &'a Path) -> BoxedFuture<Result<(), AssetWriterError>> {
362        Box::pin(Self::remove_directory(self, path))
363    }
364    fn remove_empty_directory<'a>(
365        &'a self,
366        path: &'a Path,
367    ) -> BoxedFuture<Result<(), AssetWriterError>> {
368        Box::pin(Self::remove_empty_directory(self, path))
369    }
370    fn remove_assets_in_directory<'a>(
371        &'a self,
372        path: &'a Path,
373    ) -> BoxedFuture<Result<(), AssetWriterError>> {
374        Box::pin(Self::remove_assets_in_directory(self, path))
375    }
376    fn write_bytes<'a>(
377        &'a self,
378        path: &'a Path,
379        bytes: &'a [u8],
380    ) -> BoxedFuture<Result<(), AssetWriterError>> {
381        Box::pin(Self::write_bytes(self, path, bytes))
382    }
383    fn write_meta_bytes<'a>(
384        &'a self,
385        path: &'a Path,
386        bytes: &'a [u8],
387    ) -> BoxedFuture<Result<(), AssetWriterError>> {
388        Box::pin(Self::write_meta_bytes(self, path, bytes))
389    }
390}
391
392/// An "asset source change event" that occurs whenever asset (or asset metadata) is created/added/removed
393#[derive(Clone, Debug, PartialEq, Eq)]
394pub enum AssetSourceEvent {
395    /// An asset at this path was added.
396    AddedAsset(PathBuf),
397    /// An asset at this path was modified.
398    ModifiedAsset(PathBuf),
399    /// An asset at this path was removed.
400    RemovedAsset(PathBuf),
401    /// An asset at this path was renamed.
402    RenamedAsset { old: PathBuf, new: PathBuf },
403    /// Asset metadata at this path was added.
404    AddedMeta(PathBuf),
405    /// Asset metadata at this path was modified.
406    ModifiedMeta(PathBuf),
407    /// Asset metadata at this path was removed.
408    RemovedMeta(PathBuf),
409    /// Asset metadata at this path was renamed.
410    RenamedMeta { old: PathBuf, new: PathBuf },
411    /// A folder at the given path was added.
412    AddedFolder(PathBuf),
413    /// A folder at the given path was removed.
414    RemovedFolder(PathBuf),
415    /// A folder at the given path was renamed.
416    RenamedFolder { old: PathBuf, new: PathBuf },
417    /// Something of unknown type was removed. It is the job of the event handler to determine the type.
418    /// This exists because notify-rs produces "untyped" rename events without destination paths for unwatched folders, so we can't determine the type of
419    /// the rename.
420    RemovedUnknown {
421        /// The path of the removed asset or folder (undetermined). This could be an asset path or a folder. This will not be a "meta file" path.
422        path: PathBuf,
423        /// This field is only relevant if `path` is determined to be an asset path (and therefore not a folder). If this field is `true`,
424        /// then this event corresponds to a meta removal (not an asset removal) . If `false`, then this event corresponds to an asset removal
425        /// (not a meta removal).
426        is_meta: bool,
427    },
428}
429
430/// A handle to an "asset watcher" process, that will listen for and emit [`AssetSourceEvent`] values for as long as
431/// [`AssetWatcher`] has not been dropped.
432pub trait AssetWatcher: Send + Sync + 'static {}
433
434/// An [`AsyncRead`] implementation capable of reading a [`Vec<u8>`].
435pub struct VecReader {
436    bytes: Vec<u8>,
437    bytes_read: usize,
438}
439
440impl VecReader {
441    /// Create a new [`VecReader`] for `bytes`.
442    pub fn new(bytes: Vec<u8>) -> Self {
443        Self {
444            bytes_read: 0,
445            bytes,
446        }
447    }
448}
449
450impl AsyncRead for VecReader {
451    fn poll_read(
452        mut self: Pin<&mut Self>,
453        cx: &mut std::task::Context<'_>,
454        buf: &mut [u8],
455    ) -> Poll<futures_io::Result<usize>> {
456        if self.bytes_read >= self.bytes.len() {
457            Poll::Ready(Ok(0))
458        } else {
459            let n = ready!(Pin::new(&mut &self.bytes[self.bytes_read..]).poll_read(cx, buf))?;
460            self.bytes_read += n;
461            Poll::Ready(Ok(n))
462        }
463    }
464}
465
466impl AsyncSeek for VecReader {
467    fn poll_seek(
468        mut self: Pin<&mut Self>,
469        _cx: &mut Context<'_>,
470        pos: SeekFrom,
471    ) -> Poll<std::io::Result<u64>> {
472        let result = match pos {
473            SeekFrom::Start(offset) => offset.try_into(),
474            SeekFrom::End(offset) => self.bytes.len().try_into().map(|len: i64| len - offset),
475            SeekFrom::Current(offset) => self
476                .bytes_read
477                .try_into()
478                .map(|bytes_read: i64| bytes_read + offset),
479        };
480
481        if let Ok(new_pos) = result {
482            if new_pos < 0 {
483                Poll::Ready(Err(std::io::Error::new(
484                    std::io::ErrorKind::InvalidInput,
485                    "seek position is out of range",
486                )))
487            } else {
488                self.bytes_read = new_pos as _;
489
490                Poll::Ready(Ok(new_pos as _))
491            }
492        } else {
493            Poll::Ready(Err(std::io::Error::new(
494                std::io::ErrorKind::InvalidInput,
495                "seek position is out of range",
496            )))
497        }
498    }
499}
500
501/// An [`AsyncRead`] implementation capable of reading a [`&[u8]`].
502pub struct SliceReader<'a> {
503    bytes: &'a [u8],
504    bytes_read: usize,
505}
506
507impl<'a> SliceReader<'a> {
508    /// Create a new [`SliceReader`] for `bytes`.
509    pub fn new(bytes: &'a [u8]) -> Self {
510        Self {
511            bytes,
512            bytes_read: 0,
513        }
514    }
515}
516
517impl<'a> AsyncRead for SliceReader<'a> {
518    fn poll_read(
519        mut self: Pin<&mut Self>,
520        cx: &mut Context<'_>,
521        buf: &mut [u8],
522    ) -> Poll<std::io::Result<usize>> {
523        if self.bytes_read >= self.bytes.len() {
524            Poll::Ready(Ok(0))
525        } else {
526            let n = ready!(Pin::new(&mut &self.bytes[self.bytes_read..]).poll_read(cx, buf))?;
527            self.bytes_read += n;
528            Poll::Ready(Ok(n))
529        }
530    }
531}
532
533impl<'a> AsyncSeek for SliceReader<'a> {
534    fn poll_seek(
535        mut self: Pin<&mut Self>,
536        _cx: &mut Context<'_>,
537        pos: SeekFrom,
538    ) -> Poll<std::io::Result<u64>> {
539        let result = match pos {
540            SeekFrom::Start(offset) => offset.try_into(),
541            SeekFrom::End(offset) => self.bytes.len().try_into().map(|len: i64| len - offset),
542            SeekFrom::Current(offset) => self
543                .bytes_read
544                .try_into()
545                .map(|bytes_read: i64| bytes_read + offset),
546        };
547
548        if let Ok(new_pos) = result {
549            if new_pos < 0 {
550                Poll::Ready(Err(std::io::Error::new(
551                    std::io::ErrorKind::InvalidInput,
552                    "seek position is out of range",
553                )))
554            } else {
555                self.bytes_read = new_pos as _;
556
557                Poll::Ready(Ok(new_pos as _))
558            }
559        } else {
560            Poll::Ready(Err(std::io::Error::new(
561                std::io::ErrorKind::InvalidInput,
562                "seek position is out of range",
563            )))
564        }
565    }
566}
567
568/// Appends `.meta` to the given path.
569pub(crate) fn get_meta_path(path: &Path) -> PathBuf {
570    let mut meta_path = path.to_path_buf();
571    let mut extension = path.extension().unwrap_or_default().to_os_string();
572    extension.push(".meta");
573    meta_path.set_extension(extension);
574    meta_path
575}
576
577#[cfg(any(target_arch = "wasm32", target_os = "android"))]
578/// A [`PathBuf`] [`Stream`] implementation that immediately returns nothing.
579struct EmptyPathStream;
580
581#[cfg(any(target_arch = "wasm32", target_os = "android"))]
582impl Stream for EmptyPathStream {
583    type Item = PathBuf;
584
585    fn poll_next(
586        self: Pin<&mut Self>,
587        _cx: &mut std::task::Context<'_>,
588    ) -> Poll<Option<Self::Item>> {
589        Poll::Ready(None)
590    }
591}