1#[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#[derive(Error, Debug, Clone)]
50pub enum AssetReaderError {
51 #[error("Path not found: {}", _0.display())]
53 NotFound(PathBuf),
54
55 #[error("Encountered an I/O error while loading asset: {0}")]
57 Io(Arc<std::io::Error>),
58
59 #[error("Encountered HTTP status {0:?} when loading asset")]
63 HttpError(u16),
64}
65
66impl PartialEq for AssetReaderError {
67 #[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
87pub const STACK_FUTURE_SIZE: usize = 10 * size_of::<&()>();
93
94pub use stackfuture::StackFuture;
95
96pub trait Reader: AsyncRead + Unpin + Send + Sync {
103 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 fn seekable(&mut self) -> Result<&mut dyn SeekableReader, ReaderNotSeekableError>;
145}
146
147pub trait SeekableReader: Reader + AsyncSeek {}
150
151impl<T: Reader + AsyncSeek> SeekableReader for T {}
152
153#[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
173pub 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
187pub trait AssetReader: Send + Sync + 'static {
196 fn read<'a>(&'a self, path: &'a Path) -> impl AssetReaderFuture<Value: Reader + 'a>;
218 fn read_meta<'a>(&'a self, path: &'a Path) -> impl AssetReaderFuture<Value: Reader + 'a>;
220 fn read_directory<'a>(
222 &'a self,
223 path: &'a Path,
224 ) -> impl ConditionalSendFuture<Output = Result<Box<PathStream>, AssetReaderError>>;
225 fn is_directory<'a>(
227 &'a self,
228 path: &'a Path,
229 ) -> impl ConditionalSendFuture<Output = Result<bool, AssetReaderError>>;
230 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
245pub trait ErasedAssetReader: Send + Sync + 'static {
248 fn read<'a>(
250 &'a self,
251 path: &'a Path,
252 ) -> BoxedFuture<'a, Result<Box<dyn Reader + 'a>, AssetReaderError>>;
253 fn read_meta<'a>(
255 &'a self,
256 path: &'a Path,
257 ) -> BoxedFuture<'a, Result<Box<dyn Reader + 'a>, AssetReaderError>>;
258 fn read_directory<'a>(
260 &'a self,
261 path: &'a Path,
262 ) -> BoxedFuture<'a, Result<Box<PathStream>, AssetReaderError>>;
263 fn is_directory<'a>(
265 &'a self,
266 path: &'a Path,
267 ) -> BoxedFuture<'a, Result<bool, AssetReaderError>>;
268 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
315pub type Writer = dyn AsyncWrite + Unpin + Send + Sync;
317
318pub type PathStream = dyn Stream<Item = PathBuf> + Unpin + Send;
320
321#[derive(Error, Debug)]
323pub enum AssetWriterError {
324 #[error("encountered an io error while loading asset: {0}")]
326 Io(#[from] std::io::Error),
327}
328
329pub trait AssetWriter: Send + Sync + 'static {
338 fn write<'a>(
340 &'a self,
341 path: &'a Path,
342 ) -> impl ConditionalSendFuture<Output = Result<Box<Writer>, AssetWriterError>>;
343 fn write_meta<'a>(
346 &'a self,
347 path: &'a Path,
348 ) -> impl ConditionalSendFuture<Output = Result<Box<Writer>, AssetWriterError>>;
349 fn remove<'a>(
351 &'a self,
352 path: &'a Path,
353 ) -> impl ConditionalSendFuture<Output = Result<(), AssetWriterError>>;
354 fn remove_meta<'a>(
357 &'a self,
358 path: &'a Path,
359 ) -> impl ConditionalSendFuture<Output = Result<(), AssetWriterError>>;
360 fn rename<'a>(
362 &'a self,
363 old_path: &'a Path,
364 new_path: &'a Path,
365 ) -> impl ConditionalSendFuture<Output = Result<(), AssetWriterError>>;
366 fn rename_meta<'a>(
369 &'a self,
370 old_path: &'a Path,
371 new_path: &'a Path,
372 ) -> impl ConditionalSendFuture<Output = Result<(), AssetWriterError>>;
373 fn create_directory<'a>(
376 &'a self,
377 path: &'a Path,
378 ) -> impl ConditionalSendFuture<Output = Result<(), AssetWriterError>>;
379 fn remove_directory<'a>(
381 &'a self,
382 path: &'a Path,
383 ) -> impl ConditionalSendFuture<Output = Result<(), AssetWriterError>>;
384 fn remove_empty_directory<'a>(
387 &'a self,
388 path: &'a Path,
389 ) -> impl ConditionalSendFuture<Output = Result<(), AssetWriterError>>;
390 fn remove_assets_in_directory<'a>(
392 &'a self,
393 path: &'a Path,
394 ) -> impl ConditionalSendFuture<Output = Result<(), AssetWriterError>>;
395 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 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
423pub trait ErasedAssetWriter: Send + Sync + 'static {
426 fn write<'a>(
428 &'a self,
429 path: &'a Path,
430 ) -> BoxedFuture<'a, Result<Box<Writer>, AssetWriterError>>;
431 fn write_meta<'a>(
434 &'a self,
435 path: &'a Path,
436 ) -> BoxedFuture<'a, Result<Box<Writer>, AssetWriterError>>;
437 fn remove<'a>(&'a self, path: &'a Path) -> BoxedFuture<'a, Result<(), AssetWriterError>>;
439 fn remove_meta<'a>(&'a self, path: &'a Path) -> BoxedFuture<'a, Result<(), AssetWriterError>>;
442 fn rename<'a>(
444 &'a self,
445 old_path: &'a Path,
446 new_path: &'a Path,
447 ) -> BoxedFuture<'a, Result<(), AssetWriterError>>;
448 fn rename_meta<'a>(
451 &'a self,
452 old_path: &'a Path,
453 new_path: &'a Path,
454 ) -> BoxedFuture<'a, Result<(), AssetWriterError>>;
455 fn create_directory<'a>(
458 &'a self,
459 path: &'a Path,
460 ) -> BoxedFuture<'a, Result<(), AssetWriterError>>;
461 fn remove_directory<'a>(
463 &'a self,
464 path: &'a Path,
465 ) -> BoxedFuture<'a, Result<(), AssetWriterError>>;
466 fn remove_empty_directory<'a>(
469 &'a self,
470 path: &'a Path,
471 ) -> BoxedFuture<'a, Result<(), AssetWriterError>>;
472 fn remove_assets_in_directory<'a>(
474 &'a self,
475 path: &'a Path,
476 ) -> BoxedFuture<'a, Result<(), AssetWriterError>>;
477 fn write_bytes<'a>(
479 &'a self,
480 path: &'a Path,
481 bytes: &'a [u8],
482 ) -> BoxedFuture<'a, Result<(), AssetWriterError>>;
483 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#[derive(Clone, Debug, PartialEq, Eq)]
566pub enum AssetSourceEvent {
567 AddedAsset(PathBuf),
569 ModifiedAsset(PathBuf),
571 RemovedAsset(PathBuf),
573 #[expect(missing_docs, reason = "the names are self documenting")]
575 RenamedAsset { old: PathBuf, new: PathBuf },
576 AddedMeta(PathBuf),
578 ModifiedMeta(PathBuf),
580 RemovedMeta(PathBuf),
582 #[expect(missing_docs, reason = "the names are self documenting")]
584 RenamedMeta { old: PathBuf, new: PathBuf },
585 AddedFolder(PathBuf),
587 RemovedFolder(PathBuf),
589 #[expect(missing_docs, reason = "the names are self documenting")]
591 RenamedFolder { old: PathBuf, new: PathBuf },
592 RemovedUnknown {
596 path: PathBuf,
598 is_meta: bool,
602 },
603}
604
605pub trait AssetWatcher: Send + Sync + 'static {}
608
609pub struct VecReader {
611 pub bytes: Vec<u8>,
613 bytes_read: usize,
614}
615
616impl VecReader {
617 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 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 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
663pub struct SliceReader<'a> {
665 bytes: &'a [u8],
666 bytes_read: usize,
667}
668
669impl<'a> SliceReader<'a> {
670 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
712pub(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
723pub(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
753pub(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
774pub(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")]
789struct 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}