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