Skip to main content

bevy_asset/io/
mod.rs

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