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 memory;
14pub mod processor_gated;
15#[cfg(target_arch = "wasm32")]
16pub mod wasm;
17#[cfg(any(feature = "http", feature = "https"))]
18pub mod web;
19
20#[cfg(test)]
21pub mod gated;
22
23mod source;
24
25pub use futures_lite::AsyncWriteExt;
26pub use source::*;
27
28use alloc::{boxed::Box, sync::Arc, vec::Vec};
29use bevy_tasks::{BoxedFuture, ConditionalSendFuture};
30use core::{
31    mem::size_of,
32    pin::Pin,
33    task::{Context, Poll},
34};
35use futures_io::{AsyncRead, AsyncSeek, AsyncWrite};
36use futures_lite::Stream;
37use std::{
38    io::SeekFrom,
39    path::{Path, PathBuf},
40};
41use thiserror::Error;
42
43/// Errors that occur while loading assets.
44#[derive(Error, Debug, Clone)]
45pub enum AssetReaderError {
46    /// Path not found.
47    #[error("Path not found: {}", _0.display())]
48    NotFound(PathBuf),
49
50    /// Encountered an I/O error while loading an asset.
51    #[error("Encountered an I/O error while loading asset: {0}")]
52    Io(Arc<std::io::Error>),
53
54    /// The HTTP request completed but returned an unhandled [HTTP response status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status).
55    /// - If the request returns a 404 error, expect [`AssetReaderError::NotFound`].
56    /// - If the request fails before getting a status code (e.g. request timeout, interrupted connection, etc), expect [`AssetReaderError::Io`].
57    #[error("Encountered HTTP status {0:?} when loading asset")]
58    HttpError(u16),
59}
60
61impl PartialEq for AssetReaderError {
62    /// Equality comparison for `AssetReaderError::Io` is not full (only through `ErrorKind` of inner error)
63    #[inline]
64    fn eq(&self, other: &Self) -> bool {
65        match (self, other) {
66            (Self::NotFound(path), Self::NotFound(other_path)) => path == other_path,
67            (Self::Io(error), Self::Io(other_error)) => error.kind() == other_error.kind(),
68            (Self::HttpError(code), Self::HttpError(other_code)) => code == other_code,
69            _ => false,
70        }
71    }
72}
73
74impl Eq for AssetReaderError {}
75
76impl From<std::io::Error> for AssetReaderError {
77    fn from(value: std::io::Error) -> Self {
78        Self::Io(Arc::new(value))
79    }
80}
81
82/// The maximum size of a future returned from [`Reader::read_to_end`].
83/// This is large enough to fit ten references.
84// Ideally this would be even smaller (ReadToEndFuture only needs space for two references based on its definition),
85// but compiler optimizations can apparently inflate the stack size of futures due to inlining, which makes
86// a higher maximum necessary.
87pub const STACK_FUTURE_SIZE: usize = 10 * size_of::<&()>();
88
89pub use stackfuture::StackFuture;
90
91/// A type returned from [`AssetReader::read`], which is used to read the contents of a file
92/// (or virtual file) corresponding to an asset.
93///
94/// This is essentially a trait alias for types implementing [`AsyncRead`] and [`AsyncSeek`].
95/// The only reason a blanket implementation is not provided for applicable types is to allow
96/// implementors to override the provided implementation of [`Reader::read_to_end`].
97pub trait Reader: AsyncRead + Unpin + Send + Sync {
98    /// Reads the entire contents of this reader and appends them to a vec.
99    ///
100    /// # Note for implementors
101    /// You should override the provided implementation if you can fill up the buffer more
102    /// efficiently than the default implementation, which calls `poll_read` repeatedly to
103    /// fill up the buffer 32 bytes at a time.
104    fn read_to_end<'a>(
105        &'a mut self,
106        buf: &'a mut Vec<u8>,
107    ) -> StackFuture<'a, std::io::Result<usize>, STACK_FUTURE_SIZE> {
108        let future = futures_lite::AsyncReadExt::read_to_end(self, buf);
109        StackFuture::from(future)
110    }
111
112    /// Casts this [`Reader`] as a [`SeekableReader`], which layers on [`AsyncSeek`] functionality.
113    /// Returns [`Ok`] if this [`Reader`] supports seeking. Otherwise returns [`Err`].
114    ///
115    /// Implementers of [`Reader`] are highly encouraged to provide this functionality, as it makes the
116    /// reader compatible with "seeking" [`AssetLoader`](crate::AssetLoader) implementations.
117    ///
118    /// [`AssetLoader`](crate::AssetLoader) implementations that call this are encouraged to provide fallback behavior
119    /// when it fails, such as reading into a seek-able [`Vec`] (or [`AsyncSeek`]-able [`VecReader`]):
120    ///
121    /// ```
122    /// # use bevy_asset::{io::{VecReader, Reader}, AsyncSeekExt};
123    /// # use std::{io::SeekFrom, vec::Vec};
124    /// # async {
125    /// # let mut original_reader = VecReader::new(Vec::new());
126    /// # let reader: &mut dyn Reader = &mut original_reader;
127    /// let mut fallback_reader;
128    /// let reader = match reader.seekable() {
129    ///     Ok(seek) => seek,
130    ///     Err(_) => {
131    ///         fallback_reader = VecReader::new(Vec::new());
132    ///         reader.read_to_end(&mut fallback_reader.bytes).await.unwrap();
133    ///         &mut fallback_reader
134    ///     }
135    /// };
136    /// reader.seek(SeekFrom::Start(10)).await.unwrap();
137    /// # };
138    /// ```
139    fn seekable(&mut self) -> Result<&mut dyn SeekableReader, ReaderNotSeekableError>;
140}
141
142/// A [`Reader`] that also has [`AsyncSeek`] functionality.
143/// See [`Reader::seekable`] for details.
144pub trait SeekableReader: Reader + AsyncSeek {}
145
146impl<T: Reader + AsyncSeek> SeekableReader for T {}
147
148/// Error returned by [`Reader::seekable`] when the reader implementation does not support [`AsyncSeek`] behavior.
149#[derive(Error, Debug, Copy, Clone)]
150#[error(
151    "The `Reader` returned by the current `AssetReader` does not support `AsyncSeek` behavior."
152)]
153pub struct ReaderNotSeekableError;
154
155impl Reader for Box<dyn Reader + '_> {
156    fn read_to_end<'a>(
157        &'a mut self,
158        buf: &'a mut Vec<u8>,
159    ) -> StackFuture<'a, std::io::Result<usize>, STACK_FUTURE_SIZE> {
160        (**self).read_to_end(buf)
161    }
162
163    fn seekable(&mut self) -> Result<&mut dyn SeekableReader, ReaderNotSeekableError> {
164        (**self).seekable()
165    }
166}
167
168/// A future that returns a value or an [`AssetReaderError`]
169pub trait AssetReaderFuture:
170    ConditionalSendFuture<Output = Result<Self::Value, AssetReaderError>>
171{
172    type Value;
173}
174
175impl<F, T> AssetReaderFuture for F
176where
177    F: ConditionalSendFuture<Output = Result<T, AssetReaderError>>,
178{
179    type Value = T;
180}
181
182/// Performs read operations on an asset storage. [`AssetReader`] exposes a "virtual filesystem"
183/// API, where asset bytes and asset metadata bytes are both stored and accessible for a given
184/// `path`. This trait is not object safe, if needed use a dyn [`ErasedAssetReader`] instead.
185///
186/// This trait defines asset-agnostic mechanisms to read bytes from a storage system.
187/// For the per-asset-type saving/loading logic, see [`AssetSaver`](crate::saver::AssetSaver) and [`AssetLoader`](crate::loader::AssetLoader).
188///
189/// For a complementary version of this trait that can write assets to storage, see [`AssetWriter`].
190pub trait AssetReader: Send + Sync + 'static {
191    /// Returns a future to load the full file data at the provided path.
192    ///
193    /// # Note for implementors
194    /// The preferred style for implementing this method is an `async fn` returning an opaque type.
195    ///
196    /// ```no_run
197    /// # use std::path::Path;
198    /// # use bevy_asset::{prelude::*, io::{AssetReader, PathStream, Reader, AssetReaderError}};
199    /// # struct MyReader;
200    /// impl AssetReader for MyReader {
201    ///     async fn read<'a>(&'a self, path: &'a Path) -> Result<impl Reader + 'a, AssetReaderError> {
202    ///         // ...
203    ///         # let val: Box<dyn Reader> = unimplemented!(); Ok(val)
204    ///     }
205    ///     # async fn read_meta<'a>(&'a self, path: &'a Path) -> Result<impl Reader + 'a, AssetReaderError> {
206    ///     #     let val: Box<dyn Reader> = unimplemented!(); Ok(val) }
207    ///     # async fn read_directory<'a>(&'a self, path: &'a Path) -> Result<Box<PathStream>, AssetReaderError> { unimplemented!() }
208    ///     # async fn is_directory<'a>(&'a self, path: &'a Path) -> Result<bool, AssetReaderError> { unimplemented!() }
209    ///     # async fn read_meta_bytes<'a>(&'a self, path: &'a Path) -> Result<Vec<u8>, AssetReaderError> { unimplemented!() }
210    /// }
211    /// ```
212    fn read<'a>(&'a self, path: &'a Path) -> impl AssetReaderFuture<Value: Reader + 'a>;
213    /// Returns a future to load the full file data at the provided path.
214    fn read_meta<'a>(&'a self, path: &'a Path) -> impl AssetReaderFuture<Value: Reader + 'a>;
215    /// Returns an iterator of directory entry names at the provided path.
216    fn read_directory<'a>(
217        &'a self,
218        path: &'a Path,
219    ) -> impl ConditionalSendFuture<Output = Result<Box<PathStream>, AssetReaderError>>;
220    /// Returns true if the provided path points to a directory.
221    fn is_directory<'a>(
222        &'a self,
223        path: &'a Path,
224    ) -> impl ConditionalSendFuture<Output = Result<bool, AssetReaderError>>;
225    /// Reads asset metadata bytes at the given `path` into a [`Vec<u8>`]. This is a convenience
226    /// function that wraps [`AssetReader::read_meta`] by default.
227    fn read_meta_bytes<'a>(
228        &'a self,
229        path: &'a Path,
230    ) -> impl ConditionalSendFuture<Output = Result<Vec<u8>, AssetReaderError>> {
231        async {
232            let mut meta_reader = self.read_meta(path).await?;
233            let mut meta_bytes = Vec::new();
234            meta_reader.read_to_end(&mut meta_bytes).await?;
235            Ok(meta_bytes)
236        }
237    }
238}
239
240/// Equivalent to an [`AssetReader`] but using boxed futures, necessary eg. when using a `dyn AssetReader`,
241/// as [`AssetReader`] isn't currently object safe.
242pub trait ErasedAssetReader: Send + Sync + 'static {
243    /// Returns a future to load the full file data at the provided path.
244    fn read<'a>(
245        &'a self,
246        path: &'a Path,
247    ) -> BoxedFuture<'a, Result<Box<dyn Reader + 'a>, AssetReaderError>>;
248    /// Returns a future to load the full file data at the provided path.
249    fn read_meta<'a>(
250        &'a self,
251        path: &'a Path,
252    ) -> BoxedFuture<'a, Result<Box<dyn Reader + 'a>, AssetReaderError>>;
253    /// Returns an iterator of directory entry names at the provided path.
254    fn read_directory<'a>(
255        &'a self,
256        path: &'a Path,
257    ) -> BoxedFuture<'a, Result<Box<PathStream>, AssetReaderError>>;
258    /// Returns true if the provided path points to a directory.
259    fn is_directory<'a>(
260        &'a self,
261        path: &'a Path,
262    ) -> BoxedFuture<'a, Result<bool, AssetReaderError>>;
263    /// Reads asset metadata bytes at the given `path` into a [`Vec<u8>`]. This is a convenience
264    /// function that wraps [`ErasedAssetReader::read_meta`] by default.
265    fn read_meta_bytes<'a>(
266        &'a self,
267        path: &'a Path,
268    ) -> BoxedFuture<'a, Result<Vec<u8>, AssetReaderError>>;
269}
270
271impl<T: AssetReader> ErasedAssetReader for T {
272    fn read<'a>(
273        &'a self,
274        path: &'a Path,
275    ) -> BoxedFuture<'a, Result<Box<dyn Reader + 'a>, AssetReaderError>> {
276        Box::pin(async move {
277            let reader = Self::read(self, path).await?;
278            Ok(Box::new(reader) as Box<dyn Reader>)
279        })
280    }
281    fn read_meta<'a>(
282        &'a self,
283        path: &'a Path,
284    ) -> BoxedFuture<'a, Result<Box<dyn Reader + 'a>, AssetReaderError>> {
285        Box::pin(async {
286            let reader = Self::read_meta(self, path).await?;
287            Ok(Box::new(reader) as Box<dyn Reader>)
288        })
289    }
290    fn read_directory<'a>(
291        &'a self,
292        path: &'a Path,
293    ) -> BoxedFuture<'a, Result<Box<PathStream>, AssetReaderError>> {
294        Box::pin(Self::read_directory(self, path))
295    }
296    fn is_directory<'a>(
297        &'a self,
298        path: &'a Path,
299    ) -> BoxedFuture<'a, Result<bool, AssetReaderError>> {
300        Box::pin(Self::is_directory(self, path))
301    }
302    fn read_meta_bytes<'a>(
303        &'a self,
304        path: &'a Path,
305    ) -> BoxedFuture<'a, Result<Vec<u8>, AssetReaderError>> {
306        Box::pin(Self::read_meta_bytes(self, path))
307    }
308}
309
310pub type Writer = dyn AsyncWrite + Unpin + Send + Sync;
311
312pub type PathStream = dyn Stream<Item = PathBuf> + Unpin + Send;
313
314/// Errors that occur while loading assets.
315#[derive(Error, Debug)]
316pub enum AssetWriterError {
317    /// Encountered an I/O error while loading an asset.
318    #[error("encountered an io error while loading asset: {0}")]
319    Io(#[from] std::io::Error),
320}
321
322/// Preforms write operations on an asset storage. [`AssetWriter`] exposes a "virtual filesystem"
323/// API, where asset bytes and asset metadata bytes are both stored and accessible for a given
324/// `path`. This trait is not object safe, if needed use a dyn [`ErasedAssetWriter`] instead.
325///
326/// This trait defines asset-agnostic mechanisms to write bytes to a storage system.
327/// For the per-asset-type saving/loading logic, see [`AssetSaver`](crate::saver::AssetSaver) and [`AssetLoader`](crate::loader::AssetLoader).
328///
329/// For a complementary version of this trait that can read assets from storage, see [`AssetReader`].
330pub trait AssetWriter: Send + Sync + 'static {
331    /// Writes the full asset bytes at the provided path.
332    fn write<'a>(
333        &'a self,
334        path: &'a Path,
335    ) -> impl ConditionalSendFuture<Output = Result<Box<Writer>, AssetWriterError>>;
336    /// Writes the full asset meta bytes at the provided path.
337    /// This _should not_ include storage specific extensions like `.meta`.
338    fn write_meta<'a>(
339        &'a self,
340        path: &'a Path,
341    ) -> impl ConditionalSendFuture<Output = Result<Box<Writer>, AssetWriterError>>;
342    /// Removes the asset stored at the given path.
343    fn remove<'a>(
344        &'a self,
345        path: &'a Path,
346    ) -> impl ConditionalSendFuture<Output = Result<(), AssetWriterError>>;
347    /// Removes the asset meta stored at the given path.
348    /// This _should not_ include storage specific extensions like `.meta`.
349    fn remove_meta<'a>(
350        &'a self,
351        path: &'a Path,
352    ) -> impl ConditionalSendFuture<Output = Result<(), AssetWriterError>>;
353    /// Renames the asset at `old_path` to `new_path`
354    fn rename<'a>(
355        &'a self,
356        old_path: &'a Path,
357        new_path: &'a Path,
358    ) -> impl ConditionalSendFuture<Output = Result<(), AssetWriterError>>;
359    /// Renames the asset meta for the asset at `old_path` to `new_path`.
360    /// This _should not_ include storage specific extensions like `.meta`.
361    fn rename_meta<'a>(
362        &'a self,
363        old_path: &'a Path,
364        new_path: &'a Path,
365    ) -> impl ConditionalSendFuture<Output = Result<(), AssetWriterError>>;
366    /// Creates a directory at the given path, including all parent directories if they do not
367    /// already exist.
368    fn create_directory<'a>(
369        &'a self,
370        path: &'a Path,
371    ) -> impl ConditionalSendFuture<Output = Result<(), AssetWriterError>>;
372    /// Removes the directory at the given path, including all assets _and_ directories in that directory.
373    fn remove_directory<'a>(
374        &'a self,
375        path: &'a Path,
376    ) -> impl ConditionalSendFuture<Output = Result<(), AssetWriterError>>;
377    /// Removes the directory at the given path, but only if it is completely empty. This will return an error if the
378    /// directory is not empty.
379    fn remove_empty_directory<'a>(
380        &'a self,
381        path: &'a Path,
382    ) -> impl ConditionalSendFuture<Output = Result<(), AssetWriterError>>;
383    /// Removes all assets (and directories) in this directory, resulting in an empty directory.
384    fn remove_assets_in_directory<'a>(
385        &'a self,
386        path: &'a Path,
387    ) -> impl ConditionalSendFuture<Output = Result<(), AssetWriterError>>;
388    /// Writes the asset `bytes` to the given `path`.
389    fn write_bytes<'a>(
390        &'a self,
391        path: &'a Path,
392        bytes: &'a [u8],
393    ) -> impl ConditionalSendFuture<Output = Result<(), AssetWriterError>> {
394        async {
395            let mut writer = self.write(path).await?;
396            writer.write_all(bytes).await?;
397            writer.flush().await?;
398            Ok(())
399        }
400    }
401    /// Writes the asset meta `bytes` to the given `path`.
402    fn write_meta_bytes<'a>(
403        &'a self,
404        path: &'a Path,
405        bytes: &'a [u8],
406    ) -> impl ConditionalSendFuture<Output = Result<(), AssetWriterError>> {
407        async {
408            let mut meta_writer = self.write_meta(path).await?;
409            meta_writer.write_all(bytes).await?;
410            meta_writer.flush().await?;
411            Ok(())
412        }
413    }
414}
415
416/// Equivalent to an [`AssetWriter`] but using boxed futures, necessary eg. when using a `dyn AssetWriter`,
417/// as [`AssetWriter`] isn't currently object safe.
418pub trait ErasedAssetWriter: Send + Sync + 'static {
419    /// Writes the full asset bytes at the provided path.
420    fn write<'a>(
421        &'a self,
422        path: &'a Path,
423    ) -> BoxedFuture<'a, Result<Box<Writer>, AssetWriterError>>;
424    /// Writes the full asset meta bytes at the provided path.
425    /// This _should not_ include storage specific extensions like `.meta`.
426    fn write_meta<'a>(
427        &'a self,
428        path: &'a Path,
429    ) -> BoxedFuture<'a, Result<Box<Writer>, AssetWriterError>>;
430    /// Removes the asset stored at the given path.
431    fn remove<'a>(&'a self, path: &'a Path) -> BoxedFuture<'a, Result<(), AssetWriterError>>;
432    /// Removes the asset meta stored at the given path.
433    /// This _should not_ include storage specific extensions like `.meta`.
434    fn remove_meta<'a>(&'a self, path: &'a Path) -> BoxedFuture<'a, Result<(), AssetWriterError>>;
435    /// Renames the asset at `old_path` to `new_path`
436    fn rename<'a>(
437        &'a self,
438        old_path: &'a Path,
439        new_path: &'a Path,
440    ) -> BoxedFuture<'a, Result<(), AssetWriterError>>;
441    /// Renames the asset meta for the asset at `old_path` to `new_path`.
442    /// This _should not_ include storage specific extensions like `.meta`.
443    fn rename_meta<'a>(
444        &'a self,
445        old_path: &'a Path,
446        new_path: &'a Path,
447    ) -> BoxedFuture<'a, Result<(), AssetWriterError>>;
448    /// Creates a directory at the given path, including all parent directories if they do not
449    /// already exist.
450    fn create_directory<'a>(
451        &'a self,
452        path: &'a Path,
453    ) -> BoxedFuture<'a, Result<(), AssetWriterError>>;
454    /// Removes the directory at the given path, including all assets _and_ directories in that directory.
455    fn remove_directory<'a>(
456        &'a self,
457        path: &'a Path,
458    ) -> BoxedFuture<'a, Result<(), AssetWriterError>>;
459    /// Removes the directory at the given path, but only if it is completely empty. This will return an error if the
460    /// directory is not empty.
461    fn remove_empty_directory<'a>(
462        &'a self,
463        path: &'a Path,
464    ) -> BoxedFuture<'a, Result<(), AssetWriterError>>;
465    /// Removes all assets (and directories) in this directory, resulting in an empty directory.
466    fn remove_assets_in_directory<'a>(
467        &'a self,
468        path: &'a Path,
469    ) -> BoxedFuture<'a, Result<(), AssetWriterError>>;
470    /// Writes the asset `bytes` to the given `path`.
471    fn write_bytes<'a>(
472        &'a self,
473        path: &'a Path,
474        bytes: &'a [u8],
475    ) -> BoxedFuture<'a, Result<(), AssetWriterError>>;
476    /// Writes the asset meta `bytes` to the given `path`.
477    fn write_meta_bytes<'a>(
478        &'a self,
479        path: &'a Path,
480        bytes: &'a [u8],
481    ) -> BoxedFuture<'a, Result<(), AssetWriterError>>;
482}
483
484impl<T: AssetWriter> ErasedAssetWriter for T {
485    fn write<'a>(
486        &'a self,
487        path: &'a Path,
488    ) -> BoxedFuture<'a, Result<Box<Writer>, AssetWriterError>> {
489        Box::pin(Self::write(self, path))
490    }
491    fn write_meta<'a>(
492        &'a self,
493        path: &'a Path,
494    ) -> BoxedFuture<'a, Result<Box<Writer>, AssetWriterError>> {
495        Box::pin(Self::write_meta(self, path))
496    }
497    fn remove<'a>(&'a self, path: &'a Path) -> BoxedFuture<'a, Result<(), AssetWriterError>> {
498        Box::pin(Self::remove(self, path))
499    }
500    fn remove_meta<'a>(&'a self, path: &'a Path) -> BoxedFuture<'a, Result<(), AssetWriterError>> {
501        Box::pin(Self::remove_meta(self, path))
502    }
503    fn rename<'a>(
504        &'a self,
505        old_path: &'a Path,
506        new_path: &'a Path,
507    ) -> BoxedFuture<'a, Result<(), AssetWriterError>> {
508        Box::pin(Self::rename(self, old_path, new_path))
509    }
510    fn rename_meta<'a>(
511        &'a self,
512        old_path: &'a Path,
513        new_path: &'a Path,
514    ) -> BoxedFuture<'a, Result<(), AssetWriterError>> {
515        Box::pin(Self::rename_meta(self, old_path, new_path))
516    }
517    fn create_directory<'a>(
518        &'a self,
519        path: &'a Path,
520    ) -> BoxedFuture<'a, Result<(), AssetWriterError>> {
521        Box::pin(Self::create_directory(self, path))
522    }
523    fn remove_directory<'a>(
524        &'a self,
525        path: &'a Path,
526    ) -> BoxedFuture<'a, Result<(), AssetWriterError>> {
527        Box::pin(Self::remove_directory(self, path))
528    }
529    fn remove_empty_directory<'a>(
530        &'a self,
531        path: &'a Path,
532    ) -> BoxedFuture<'a, Result<(), AssetWriterError>> {
533        Box::pin(Self::remove_empty_directory(self, path))
534    }
535    fn remove_assets_in_directory<'a>(
536        &'a self,
537        path: &'a Path,
538    ) -> BoxedFuture<'a, Result<(), AssetWriterError>> {
539        Box::pin(Self::remove_assets_in_directory(self, path))
540    }
541    fn write_bytes<'a>(
542        &'a self,
543        path: &'a Path,
544        bytes: &'a [u8],
545    ) -> BoxedFuture<'a, Result<(), AssetWriterError>> {
546        Box::pin(Self::write_bytes(self, path, bytes))
547    }
548    fn write_meta_bytes<'a>(
549        &'a self,
550        path: &'a Path,
551        bytes: &'a [u8],
552    ) -> BoxedFuture<'a, Result<(), AssetWriterError>> {
553        Box::pin(Self::write_meta_bytes(self, path, bytes))
554    }
555}
556
557/// An "asset source change event" that occurs whenever asset (or asset metadata) is created/added/removed
558#[derive(Clone, Debug, PartialEq, Eq)]
559pub enum AssetSourceEvent {
560    /// An asset at this path was added.
561    AddedAsset(PathBuf),
562    /// An asset at this path was modified.
563    ModifiedAsset(PathBuf),
564    /// An asset at this path was removed.
565    RemovedAsset(PathBuf),
566    /// An asset at this path was renamed.
567    RenamedAsset { old: PathBuf, new: PathBuf },
568    /// Asset metadata at this path was added.
569    AddedMeta(PathBuf),
570    /// Asset metadata at this path was modified.
571    ModifiedMeta(PathBuf),
572    /// Asset metadata at this path was removed.
573    RemovedMeta(PathBuf),
574    /// Asset metadata at this path was renamed.
575    RenamedMeta { old: PathBuf, new: PathBuf },
576    /// A folder at the given path was added.
577    AddedFolder(PathBuf),
578    /// A folder at the given path was removed.
579    RemovedFolder(PathBuf),
580    /// A folder at the given path was renamed.
581    RenamedFolder { old: PathBuf, new: PathBuf },
582    /// Something of unknown type was removed. It is the job of the event handler to determine the type.
583    /// This exists because notify-rs produces "untyped" rename events without destination paths for unwatched folders, so we can't determine the type of
584    /// the rename.
585    RemovedUnknown {
586        /// 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.
587        path: PathBuf,
588        /// This field is only relevant if `path` is determined to be an asset path (and therefore not a folder). If this field is `true`,
589        /// then this event corresponds to a meta removal (not an asset removal) . If `false`, then this event corresponds to an asset removal
590        /// (not a meta removal).
591        is_meta: bool,
592    },
593}
594
595/// A handle to an "asset watcher" process, that will listen for and emit [`AssetSourceEvent`] values for as long as
596/// [`AssetWatcher`] has not been dropped.
597pub trait AssetWatcher: Send + Sync + 'static {}
598
599/// An [`AsyncRead`] implementation capable of reading a [`Vec<u8>`].
600pub struct VecReader {
601    /// The bytes being read. This is the full original list of bytes.
602    pub bytes: Vec<u8>,
603    bytes_read: usize,
604}
605
606impl VecReader {
607    /// Create a new [`VecReader`] for `bytes`.
608    pub fn new(bytes: Vec<u8>) -> Self {
609        Self {
610            bytes_read: 0,
611            bytes,
612        }
613    }
614}
615
616impl AsyncRead for VecReader {
617    fn poll_read(
618        self: Pin<&mut Self>,
619        _cx: &mut Context<'_>,
620        buf: &mut [u8],
621    ) -> Poll<futures_io::Result<usize>> {
622        // Get the mut borrow to avoid trying to borrow the pin itself multiple times.
623        let this = self.get_mut();
624        Poll::Ready(Ok(slice_read(&this.bytes, &mut this.bytes_read, buf)))
625    }
626}
627
628impl AsyncSeek for VecReader {
629    fn poll_seek(
630        self: Pin<&mut Self>,
631        _cx: &mut Context<'_>,
632        pos: SeekFrom,
633    ) -> Poll<std::io::Result<u64>> {
634        // Get the mut borrow to avoid trying to borrow the pin itself multiple times.
635        let this = self.get_mut();
636        Poll::Ready(slice_seek(&this.bytes, &mut this.bytes_read, pos))
637    }
638}
639
640impl Reader for VecReader {
641    fn read_to_end<'a>(
642        &'a mut self,
643        buf: &'a mut Vec<u8>,
644    ) -> StackFuture<'a, std::io::Result<usize>, STACK_FUTURE_SIZE> {
645        read_to_end(&self.bytes, &mut self.bytes_read, buf)
646    }
647
648    fn seekable(&mut self) -> Result<&mut dyn SeekableReader, ReaderNotSeekableError> {
649        Ok(self)
650    }
651}
652
653/// An [`AsyncRead`] implementation capable of reading a [`&[u8]`].
654pub struct SliceReader<'a> {
655    bytes: &'a [u8],
656    bytes_read: usize,
657}
658
659impl<'a> SliceReader<'a> {
660    /// Create a new [`SliceReader`] for `bytes`.
661    pub fn new(bytes: &'a [u8]) -> Self {
662        Self {
663            bytes,
664            bytes_read: 0,
665        }
666    }
667}
668
669impl<'a> AsyncRead for SliceReader<'a> {
670    fn poll_read(
671        mut self: Pin<&mut Self>,
672        _cx: &mut Context<'_>,
673        buf: &mut [u8],
674    ) -> Poll<std::io::Result<usize>> {
675        Poll::Ready(Ok(slice_read(self.bytes, &mut self.bytes_read, buf)))
676    }
677}
678
679impl<'a> AsyncSeek for SliceReader<'a> {
680    fn poll_seek(
681        mut self: Pin<&mut Self>,
682        _cx: &mut Context<'_>,
683        pos: SeekFrom,
684    ) -> Poll<std::io::Result<u64>> {
685        Poll::Ready(slice_seek(self.bytes, &mut self.bytes_read, pos))
686    }
687}
688
689impl Reader for SliceReader<'_> {
690    fn read_to_end<'a>(
691        &'a mut self,
692        buf: &'a mut Vec<u8>,
693    ) -> StackFuture<'a, std::io::Result<usize>, STACK_FUTURE_SIZE> {
694        read_to_end(self.bytes, &mut self.bytes_read, buf)
695    }
696
697    fn seekable(&mut self) -> Result<&mut dyn SeekableReader, ReaderNotSeekableError> {
698        Ok(self)
699    }
700}
701
702/// Performs a read from the `slice` into `buf`.
703pub(crate) fn slice_read(slice: &[u8], bytes_read: &mut usize, buf: &mut [u8]) -> usize {
704    if *bytes_read >= slice.len() {
705        0
706    } else {
707        let n = std::io::Read::read(&mut &slice[(*bytes_read)..], buf).unwrap();
708        *bytes_read += n;
709        n
710    }
711}
712
713/// Performs a "seek" and updates the cursor of `bytes_read`. Returns the new byte position.
714pub(crate) fn slice_seek(
715    slice: &[u8],
716    bytes_read: &mut usize,
717    pos: SeekFrom,
718) -> std::io::Result<u64> {
719    let make_error = || {
720        Err(std::io::Error::new(
721            std::io::ErrorKind::InvalidInput,
722            "seek position is out of range",
723        ))
724    };
725    let (origin, offset) = match pos {
726        SeekFrom::Current(offset) => (*bytes_read, Ok(offset)),
727        SeekFrom::Start(offset) => (0, offset.try_into()),
728        SeekFrom::End(offset) => (slice.len(), Ok(offset)),
729    };
730    let Ok(offset) = offset else {
731        return make_error();
732    };
733    let Ok(origin): Result<i64, _> = origin.try_into() else {
734        return make_error();
735    };
736    let Ok(new_pos) = (origin + offset).try_into() else {
737        return make_error();
738    };
739    *bytes_read = new_pos;
740    Ok(new_pos as _)
741}
742
743/// Copies bytes from source to dest, keeping track of where in the source it starts copying from.
744///
745/// This is effectively the impl for [`SliceReader::read_to_end`], but this is provided here so the
746/// lifetimes are only tied to the buffer and not the [`SliceReader`] itself.
747pub(crate) fn read_to_end<'a>(
748    source: &'a [u8],
749    bytes_read: &'a mut usize,
750    dest: &'a mut Vec<u8>,
751) -> StackFuture<'a, std::io::Result<usize>, STACK_FUTURE_SIZE> {
752    StackFuture::from(async {
753        if *bytes_read >= source.len() {
754            Ok(0)
755        } else {
756            dest.extend_from_slice(&source[*bytes_read..]);
757            let n = source.len() - *bytes_read;
758            *bytes_read = source.len();
759            Ok(n)
760        }
761    })
762}
763
764/// Appends `.meta` to the given path:
765/// - `foo` becomes `foo.meta`
766/// - `foo.bar` becomes `foo.bar.meta`
767pub(crate) fn get_meta_path(path: &Path) -> PathBuf {
768    let mut meta_path = path.to_path_buf();
769    let mut extension = path.extension().unwrap_or_default().to_os_string();
770    if !extension.is_empty() {
771        extension.push(".");
772    }
773    extension.push("meta");
774    meta_path.set_extension(extension);
775    meta_path
776}
777
778#[cfg(any(target_arch = "wasm32", target_os = "android"))]
779/// A [`PathBuf`] [`Stream`] implementation that immediately returns nothing.
780struct EmptyPathStream;
781
782#[cfg(any(target_arch = "wasm32", target_os = "android"))]
783impl Stream for EmptyPathStream {
784    type Item = PathBuf;
785
786    fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
787        Poll::Ready(None)
788    }
789}
790
791#[cfg(test)]
792mod tests {
793    use super::*;
794
795    #[test]
796    fn get_meta_path_no_extension() {
797        assert_eq!(
798            get_meta_path(Path::new("foo")).to_str().unwrap(),
799            "foo.meta"
800        );
801    }
802
803    #[test]
804    fn get_meta_path_with_extension() {
805        assert_eq!(
806            get_meta_path(Path::new("foo.bar")).to_str().unwrap(),
807            "foo.bar.meta"
808        );
809    }
810}