1#[cfg(all(feature = "file_watcher", target_arch = "wasm32"))]
2compile_error!(
3 "The \"file_watcher\" feature for hot reloading does not work \
4 on WASM.\nDisable \"file_watcher\" \
5 when compiling to WASM"
6);
7
8#[cfg(target_os = "android")]
9pub mod android;
10pub mod embedded;
11#[cfg(not(target_arch = "wasm32"))]
12pub mod file;
13pub mod gated;
14pub mod memory;
15pub mod processor_gated;
16#[cfg(target_arch = "wasm32")]
17pub mod wasm;
18
19mod source;
20
21pub use futures_lite::{AsyncReadExt, AsyncWriteExt};
22pub use source::*;
23
24use bevy_utils::{BoxedFuture, ConditionalSendFuture};
25use futures_io::{AsyncRead, AsyncSeek, AsyncWrite};
26use futures_lite::{ready, Stream};
27use std::io::SeekFrom;
28use std::task::Context;
29use std::{
30 path::{Path, PathBuf},
31 pin::Pin,
32 sync::Arc,
33 task::Poll,
34};
35use thiserror::Error;
36
37#[derive(Error, Debug, Clone)]
39pub enum AssetReaderError {
40 #[error("Path not found: {0}")]
42 NotFound(PathBuf),
43
44 #[error("Encountered an I/O error while loading asset: {0}")]
46 Io(Arc<std::io::Error>),
47
48 #[error("Encountered HTTP status {0:?} when loading asset")]
51 HttpError(u16),
52}
53
54impl PartialEq for AssetReaderError {
55 #[inline]
57 fn eq(&self, other: &Self) -> bool {
58 match (self, other) {
59 (Self::NotFound(path), Self::NotFound(other_path)) => path == other_path,
60 (Self::Io(error), Self::Io(other_error)) => error.kind() == other_error.kind(),
61 (Self::HttpError(code), Self::HttpError(other_code)) => code == other_code,
62 _ => false,
63 }
64 }
65}
66
67impl Eq for AssetReaderError {}
68
69impl From<std::io::Error> for AssetReaderError {
70 fn from(value: std::io::Error) -> Self {
71 Self::Io(Arc::new(value))
72 }
73}
74
75pub trait AsyncReadAndSeek: AsyncRead + AsyncSeek {}
76
77impl<T: AsyncRead + AsyncSeek> AsyncReadAndSeek for T {}
78
79pub type Reader<'a> = dyn AsyncReadAndSeek + Unpin + Send + Sync + 'a;
80
81pub trait AssetReader: Send + Sync + 'static {
87 fn read<'a>(
89 &'a self,
90 path: &'a Path,
91 ) -> impl ConditionalSendFuture<Output = Result<Box<Reader<'a>>, AssetReaderError>>;
92 fn read_meta<'a>(
94 &'a self,
95 path: &'a Path,
96 ) -> impl ConditionalSendFuture<Output = Result<Box<Reader<'a>>, AssetReaderError>>;
97 fn read_directory<'a>(
99 &'a self,
100 path: &'a Path,
101 ) -> impl ConditionalSendFuture<Output = Result<Box<PathStream>, AssetReaderError>>;
102 fn is_directory<'a>(
104 &'a self,
105 path: &'a Path,
106 ) -> impl ConditionalSendFuture<Output = Result<bool, AssetReaderError>>;
107 fn read_meta_bytes<'a>(
110 &'a self,
111 path: &'a Path,
112 ) -> impl ConditionalSendFuture<Output = Result<Vec<u8>, AssetReaderError>> {
113 async {
114 let mut meta_reader = self.read_meta(path).await?;
115 let mut meta_bytes = Vec::new();
116 meta_reader.read_to_end(&mut meta_bytes).await?;
117 Ok(meta_bytes)
118 }
119 }
120}
121
122pub trait ErasedAssetReader: Send + Sync + 'static {
125 fn read<'a>(&'a self, path: &'a Path)
127 -> BoxedFuture<Result<Box<Reader<'a>>, AssetReaderError>>;
128 fn read_meta<'a>(
130 &'a self,
131 path: &'a Path,
132 ) -> BoxedFuture<Result<Box<Reader<'a>>, AssetReaderError>>;
133 fn read_directory<'a>(
135 &'a self,
136 path: &'a Path,
137 ) -> BoxedFuture<Result<Box<PathStream>, AssetReaderError>>;
138 fn is_directory<'a>(&'a self, path: &'a Path) -> BoxedFuture<Result<bool, AssetReaderError>>;
140 fn read_meta_bytes<'a>(
143 &'a self,
144 path: &'a Path,
145 ) -> BoxedFuture<Result<Vec<u8>, AssetReaderError>>;
146}
147
148impl<T: AssetReader> ErasedAssetReader for T {
149 fn read<'a>(
150 &'a self,
151 path: &'a Path,
152 ) -> BoxedFuture<Result<Box<Reader<'a>>, AssetReaderError>> {
153 Box::pin(Self::read(self, path))
154 }
155 fn read_meta<'a>(
156 &'a self,
157 path: &'a Path,
158 ) -> BoxedFuture<Result<Box<Reader<'a>>, AssetReaderError>> {
159 Box::pin(Self::read_meta(self, path))
160 }
161 fn read_directory<'a>(
162 &'a self,
163 path: &'a Path,
164 ) -> BoxedFuture<Result<Box<PathStream>, AssetReaderError>> {
165 Box::pin(Self::read_directory(self, path))
166 }
167 fn is_directory<'a>(&'a self, path: &'a Path) -> BoxedFuture<Result<bool, AssetReaderError>> {
168 Box::pin(Self::is_directory(self, path))
169 }
170 fn read_meta_bytes<'a>(
171 &'a self,
172 path: &'a Path,
173 ) -> BoxedFuture<Result<Vec<u8>, AssetReaderError>> {
174 Box::pin(Self::read_meta_bytes(self, path))
175 }
176}
177
178pub type Writer = dyn AsyncWrite + Unpin + Send + Sync;
179
180pub type PathStream = dyn Stream<Item = PathBuf> + Unpin + Send;
181
182#[derive(Error, Debug)]
184pub enum AssetWriterError {
185 #[error("encountered an io error while loading asset: {0}")]
187 Io(#[from] std::io::Error),
188}
189
190pub trait AssetWriter: Send + Sync + 'static {
196 fn write<'a>(
198 &'a self,
199 path: &'a Path,
200 ) -> impl ConditionalSendFuture<Output = Result<Box<Writer>, AssetWriterError>>;
201 fn write_meta<'a>(
204 &'a self,
205 path: &'a Path,
206 ) -> impl ConditionalSendFuture<Output = Result<Box<Writer>, AssetWriterError>>;
207 fn remove<'a>(
209 &'a self,
210 path: &'a Path,
211 ) -> impl ConditionalSendFuture<Output = Result<(), AssetWriterError>>;
212 fn remove_meta<'a>(
215 &'a self,
216 path: &'a Path,
217 ) -> impl ConditionalSendFuture<Output = Result<(), AssetWriterError>>;
218 fn rename<'a>(
220 &'a self,
221 old_path: &'a Path,
222 new_path: &'a Path,
223 ) -> impl ConditionalSendFuture<Output = Result<(), AssetWriterError>>;
224 fn rename_meta<'a>(
227 &'a self,
228 old_path: &'a Path,
229 new_path: &'a Path,
230 ) -> impl ConditionalSendFuture<Output = Result<(), AssetWriterError>>;
231 fn remove_directory<'a>(
233 &'a self,
234 path: &'a Path,
235 ) -> impl ConditionalSendFuture<Output = Result<(), AssetWriterError>>;
236 fn remove_empty_directory<'a>(
239 &'a self,
240 path: &'a Path,
241 ) -> impl ConditionalSendFuture<Output = Result<(), AssetWriterError>>;
242 fn remove_assets_in_directory<'a>(
244 &'a self,
245 path: &'a Path,
246 ) -> impl ConditionalSendFuture<Output = Result<(), AssetWriterError>>;
247 fn write_bytes<'a>(
249 &'a self,
250 path: &'a Path,
251 bytes: &'a [u8],
252 ) -> impl ConditionalSendFuture<Output = Result<(), AssetWriterError>> {
253 async {
254 let mut writer = self.write(path).await?;
255 writer.write_all(bytes).await?;
256 writer.flush().await?;
257 Ok(())
258 }
259 }
260 fn write_meta_bytes<'a>(
262 &'a self,
263 path: &'a Path,
264 bytes: &'a [u8],
265 ) -> impl ConditionalSendFuture<Output = Result<(), AssetWriterError>> {
266 async {
267 let mut meta_writer = self.write_meta(path).await?;
268 meta_writer.write_all(bytes).await?;
269 meta_writer.flush().await?;
270 Ok(())
271 }
272 }
273}
274
275pub trait ErasedAssetWriter: Send + Sync + 'static {
278 fn write<'a>(&'a self, path: &'a Path) -> BoxedFuture<Result<Box<Writer>, AssetWriterError>>;
280 fn write_meta<'a>(
283 &'a self,
284 path: &'a Path,
285 ) -> BoxedFuture<Result<Box<Writer>, AssetWriterError>>;
286 fn remove<'a>(&'a self, path: &'a Path) -> BoxedFuture<Result<(), AssetWriterError>>;
288 fn remove_meta<'a>(&'a self, path: &'a Path) -> BoxedFuture<Result<(), AssetWriterError>>;
291 fn rename<'a>(
293 &'a self,
294 old_path: &'a Path,
295 new_path: &'a Path,
296 ) -> BoxedFuture<Result<(), AssetWriterError>>;
297 fn rename_meta<'a>(
300 &'a self,
301 old_path: &'a Path,
302 new_path: &'a Path,
303 ) -> BoxedFuture<Result<(), AssetWriterError>>;
304 fn remove_directory<'a>(&'a self, path: &'a Path) -> BoxedFuture<Result<(), AssetWriterError>>;
306 fn remove_empty_directory<'a>(
309 &'a self,
310 path: &'a Path,
311 ) -> BoxedFuture<Result<(), AssetWriterError>>;
312 fn remove_assets_in_directory<'a>(
314 &'a self,
315 path: &'a Path,
316 ) -> BoxedFuture<Result<(), AssetWriterError>>;
317 fn write_bytes<'a>(
319 &'a self,
320 path: &'a Path,
321 bytes: &'a [u8],
322 ) -> BoxedFuture<Result<(), AssetWriterError>>;
323 fn write_meta_bytes<'a>(
325 &'a self,
326 path: &'a Path,
327 bytes: &'a [u8],
328 ) -> BoxedFuture<Result<(), AssetWriterError>>;
329}
330
331impl<T: AssetWriter> ErasedAssetWriter for T {
332 fn write<'a>(&'a self, path: &'a Path) -> BoxedFuture<Result<Box<Writer>, AssetWriterError>> {
333 Box::pin(Self::write(self, path))
334 }
335 fn write_meta<'a>(
336 &'a self,
337 path: &'a Path,
338 ) -> BoxedFuture<Result<Box<Writer>, AssetWriterError>> {
339 Box::pin(Self::write_meta(self, path))
340 }
341 fn remove<'a>(&'a self, path: &'a Path) -> BoxedFuture<Result<(), AssetWriterError>> {
342 Box::pin(Self::remove(self, path))
343 }
344 fn remove_meta<'a>(&'a self, path: &'a Path) -> BoxedFuture<Result<(), AssetWriterError>> {
345 Box::pin(Self::remove_meta(self, path))
346 }
347 fn rename<'a>(
348 &'a self,
349 old_path: &'a Path,
350 new_path: &'a Path,
351 ) -> BoxedFuture<Result<(), AssetWriterError>> {
352 Box::pin(Self::rename(self, old_path, new_path))
353 }
354 fn rename_meta<'a>(
355 &'a self,
356 old_path: &'a Path,
357 new_path: &'a Path,
358 ) -> BoxedFuture<Result<(), AssetWriterError>> {
359 Box::pin(Self::rename_meta(self, old_path, new_path))
360 }
361 fn remove_directory<'a>(&'a self, path: &'a Path) -> BoxedFuture<Result<(), AssetWriterError>> {
362 Box::pin(Self::remove_directory(self, path))
363 }
364 fn remove_empty_directory<'a>(
365 &'a self,
366 path: &'a Path,
367 ) -> BoxedFuture<Result<(), AssetWriterError>> {
368 Box::pin(Self::remove_empty_directory(self, path))
369 }
370 fn remove_assets_in_directory<'a>(
371 &'a self,
372 path: &'a Path,
373 ) -> BoxedFuture<Result<(), AssetWriterError>> {
374 Box::pin(Self::remove_assets_in_directory(self, path))
375 }
376 fn write_bytes<'a>(
377 &'a self,
378 path: &'a Path,
379 bytes: &'a [u8],
380 ) -> BoxedFuture<Result<(), AssetWriterError>> {
381 Box::pin(Self::write_bytes(self, path, bytes))
382 }
383 fn write_meta_bytes<'a>(
384 &'a self,
385 path: &'a Path,
386 bytes: &'a [u8],
387 ) -> BoxedFuture<Result<(), AssetWriterError>> {
388 Box::pin(Self::write_meta_bytes(self, path, bytes))
389 }
390}
391
392#[derive(Clone, Debug, PartialEq, Eq)]
394pub enum AssetSourceEvent {
395 AddedAsset(PathBuf),
397 ModifiedAsset(PathBuf),
399 RemovedAsset(PathBuf),
401 RenamedAsset { old: PathBuf, new: PathBuf },
403 AddedMeta(PathBuf),
405 ModifiedMeta(PathBuf),
407 RemovedMeta(PathBuf),
409 RenamedMeta { old: PathBuf, new: PathBuf },
411 AddedFolder(PathBuf),
413 RemovedFolder(PathBuf),
415 RenamedFolder { old: PathBuf, new: PathBuf },
417 RemovedUnknown {
421 path: PathBuf,
423 is_meta: bool,
427 },
428}
429
430pub trait AssetWatcher: Send + Sync + 'static {}
433
434pub struct VecReader {
436 bytes: Vec<u8>,
437 bytes_read: usize,
438}
439
440impl VecReader {
441 pub fn new(bytes: Vec<u8>) -> Self {
443 Self {
444 bytes_read: 0,
445 bytes,
446 }
447 }
448}
449
450impl AsyncRead for VecReader {
451 fn poll_read(
452 mut self: Pin<&mut Self>,
453 cx: &mut std::task::Context<'_>,
454 buf: &mut [u8],
455 ) -> Poll<futures_io::Result<usize>> {
456 if self.bytes_read >= self.bytes.len() {
457 Poll::Ready(Ok(0))
458 } else {
459 let n = ready!(Pin::new(&mut &self.bytes[self.bytes_read..]).poll_read(cx, buf))?;
460 self.bytes_read += n;
461 Poll::Ready(Ok(n))
462 }
463 }
464}
465
466impl AsyncSeek for VecReader {
467 fn poll_seek(
468 mut self: Pin<&mut Self>,
469 _cx: &mut Context<'_>,
470 pos: SeekFrom,
471 ) -> Poll<std::io::Result<u64>> {
472 let result = match pos {
473 SeekFrom::Start(offset) => offset.try_into(),
474 SeekFrom::End(offset) => self.bytes.len().try_into().map(|len: i64| len - offset),
475 SeekFrom::Current(offset) => self
476 .bytes_read
477 .try_into()
478 .map(|bytes_read: i64| bytes_read + offset),
479 };
480
481 if let Ok(new_pos) = result {
482 if new_pos < 0 {
483 Poll::Ready(Err(std::io::Error::new(
484 std::io::ErrorKind::InvalidInput,
485 "seek position is out of range",
486 )))
487 } else {
488 self.bytes_read = new_pos as _;
489
490 Poll::Ready(Ok(new_pos as _))
491 }
492 } else {
493 Poll::Ready(Err(std::io::Error::new(
494 std::io::ErrorKind::InvalidInput,
495 "seek position is out of range",
496 )))
497 }
498 }
499}
500
501pub struct SliceReader<'a> {
503 bytes: &'a [u8],
504 bytes_read: usize,
505}
506
507impl<'a> SliceReader<'a> {
508 pub fn new(bytes: &'a [u8]) -> Self {
510 Self {
511 bytes,
512 bytes_read: 0,
513 }
514 }
515}
516
517impl<'a> AsyncRead for SliceReader<'a> {
518 fn poll_read(
519 mut self: Pin<&mut Self>,
520 cx: &mut Context<'_>,
521 buf: &mut [u8],
522 ) -> Poll<std::io::Result<usize>> {
523 if self.bytes_read >= self.bytes.len() {
524 Poll::Ready(Ok(0))
525 } else {
526 let n = ready!(Pin::new(&mut &self.bytes[self.bytes_read..]).poll_read(cx, buf))?;
527 self.bytes_read += n;
528 Poll::Ready(Ok(n))
529 }
530 }
531}
532
533impl<'a> AsyncSeek for SliceReader<'a> {
534 fn poll_seek(
535 mut self: Pin<&mut Self>,
536 _cx: &mut Context<'_>,
537 pos: SeekFrom,
538 ) -> Poll<std::io::Result<u64>> {
539 let result = match pos {
540 SeekFrom::Start(offset) => offset.try_into(),
541 SeekFrom::End(offset) => self.bytes.len().try_into().map(|len: i64| len - offset),
542 SeekFrom::Current(offset) => self
543 .bytes_read
544 .try_into()
545 .map(|bytes_read: i64| bytes_read + offset),
546 };
547
548 if let Ok(new_pos) = result {
549 if new_pos < 0 {
550 Poll::Ready(Err(std::io::Error::new(
551 std::io::ErrorKind::InvalidInput,
552 "seek position is out of range",
553 )))
554 } else {
555 self.bytes_read = new_pos as _;
556
557 Poll::Ready(Ok(new_pos as _))
558 }
559 } else {
560 Poll::Ready(Err(std::io::Error::new(
561 std::io::ErrorKind::InvalidInput,
562 "seek position is out of range",
563 )))
564 }
565 }
566}
567
568pub(crate) fn get_meta_path(path: &Path) -> PathBuf {
570 let mut meta_path = path.to_path_buf();
571 let mut extension = path.extension().unwrap_or_default().to_os_string();
572 extension.push(".meta");
573 meta_path.set_extension(extension);
574 meta_path
575}
576
577#[cfg(any(target_arch = "wasm32", target_os = "android"))]
578struct EmptyPathStream;
580
581#[cfg(any(target_arch = "wasm32", target_os = "android"))]
582impl Stream for EmptyPathStream {
583 type Item = PathBuf;
584
585 fn poll_next(
586 self: Pin<&mut Self>,
587 _cx: &mut std::task::Context<'_>,
588 ) -> Poll<Option<Self::Item>> {
589 Poll::Ready(None)
590 }
591}