#[cfg(all(feature = "file_watcher", target_arch = "wasm32"))]
compile_error!(
"The \"file_watcher\" feature for hot reloading does not work \
on Wasm.\nDisable \"file_watcher\" \
when compiling to Wasm"
);
#[cfg(target_os = "android")]
pub mod android;
pub mod embedded;
#[cfg(not(target_arch = "wasm32"))]
pub mod file;
pub mod memory;
pub mod processor_gated;
#[cfg(target_arch = "wasm32")]
pub mod wasm;
#[cfg(any(feature = "http", feature = "https"))]
pub mod web;
#[cfg(test)]
pub mod gated;
mod source;
pub use futures_lite::AsyncWriteExt;
pub use source::*;
use alloc::{boxed::Box, sync::Arc, vec::Vec};
use bevy_tasks::{BoxedFuture, ConditionalSendFuture};
use core::{
mem::size_of,
pin::Pin,
task::{Context, Poll},
};
use futures_io::{AsyncRead, AsyncSeek, AsyncWrite};
use futures_lite::Stream;
use std::{
io::SeekFrom,
path::{Path, PathBuf},
};
use thiserror::Error;
#[derive(Error, Debug, Clone)]
pub enum AssetReaderError {
#[error("Path not found: {}", _0.display())]
NotFound(PathBuf),
#[error("Encountered an I/O error while loading asset: {0}")]
Io(Arc<std::io::Error>),
#[error("Encountered HTTP status {0:?} when loading asset")]
HttpError(u16),
}
impl PartialEq for AssetReaderError {
#[inline]
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::NotFound(path), Self::NotFound(other_path)) => path == other_path,
(Self::Io(error), Self::Io(other_error)) => error.kind() == other_error.kind(),
(Self::HttpError(code), Self::HttpError(other_code)) => code == other_code,
_ => false,
}
}
}
impl Eq for AssetReaderError {}
impl From<std::io::Error> for AssetReaderError {
fn from(value: std::io::Error) -> Self {
Self::Io(Arc::new(value))
}
}
pub const STACK_FUTURE_SIZE: usize = 10 * size_of::<&()>();
pub use stackfuture::StackFuture;
pub trait Reader: AsyncRead + Unpin + Send + Sync {
fn read_to_end<'a>(
&'a mut self,
buf: &'a mut Vec<u8>,
) -> StackFuture<'a, std::io::Result<usize>, STACK_FUTURE_SIZE> {
let future = futures_lite::AsyncReadExt::read_to_end(self, buf);
StackFuture::from(future)
}
fn seekable(&mut self) -> Result<&mut dyn SeekableReader, ReaderNotSeekableError>;
}
pub trait SeekableReader: Reader + AsyncSeek {}
impl<T: Reader + AsyncSeek> SeekableReader for T {}
#[derive(Error, Debug, Copy, Clone)]
#[error(
"The `Reader` returned by the current `AssetReader` does not support `AsyncSeek` behavior."
)]
pub struct ReaderNotSeekableError;
impl Reader for Box<dyn Reader + '_> {
fn read_to_end<'a>(
&'a mut self,
buf: &'a mut Vec<u8>,
) -> StackFuture<'a, std::io::Result<usize>, STACK_FUTURE_SIZE> {
(**self).read_to_end(buf)
}
fn seekable(&mut self) -> Result<&mut dyn SeekableReader, ReaderNotSeekableError> {
(**self).seekable()
}
}
pub trait AssetReaderFuture:
ConditionalSendFuture<Output = Result<Self::Value, AssetReaderError>>
{
type Value;
}
impl<F, T> AssetReaderFuture for F
where
F: ConditionalSendFuture<Output = Result<T, AssetReaderError>>,
{
type Value = T;
}
pub trait AssetReader: Send + Sync + 'static {
fn read<'a>(&'a self, path: &'a Path) -> impl AssetReaderFuture<Value: Reader + 'a>;
fn read_meta<'a>(&'a self, path: &'a Path) -> impl AssetReaderFuture<Value: Reader + 'a>;
fn read_directory<'a>(
&'a self,
path: &'a Path,
) -> impl ConditionalSendFuture<Output = Result<Box<PathStream>, AssetReaderError>>;
fn is_directory<'a>(
&'a self,
path: &'a Path,
) -> impl ConditionalSendFuture<Output = Result<bool, AssetReaderError>>;
fn read_meta_bytes<'a>(
&'a self,
path: &'a Path,
) -> impl ConditionalSendFuture<Output = Result<Vec<u8>, AssetReaderError>> {
async {
let mut meta_reader = self.read_meta(path).await?;
let mut meta_bytes = Vec::new();
meta_reader.read_to_end(&mut meta_bytes).await?;
Ok(meta_bytes)
}
}
}
pub trait ErasedAssetReader: Send + Sync + 'static {
fn read<'a>(
&'a self,
path: &'a Path,
) -> BoxedFuture<'a, Result<Box<dyn Reader + 'a>, AssetReaderError>>;
fn read_meta<'a>(
&'a self,
path: &'a Path,
) -> BoxedFuture<'a, Result<Box<dyn Reader + 'a>, AssetReaderError>>;
fn read_directory<'a>(
&'a self,
path: &'a Path,
) -> BoxedFuture<'a, Result<Box<PathStream>, AssetReaderError>>;
fn is_directory<'a>(
&'a self,
path: &'a Path,
) -> BoxedFuture<'a, Result<bool, AssetReaderError>>;
fn read_meta_bytes<'a>(
&'a self,
path: &'a Path,
) -> BoxedFuture<'a, Result<Vec<u8>, AssetReaderError>>;
}
impl<T: AssetReader> ErasedAssetReader for T {
fn read<'a>(
&'a self,
path: &'a Path,
) -> BoxedFuture<'a, Result<Box<dyn Reader + 'a>, AssetReaderError>> {
Box::pin(async move {
let reader = Self::read(self, path).await?;
Ok(Box::new(reader) as Box<dyn Reader>)
})
}
fn read_meta<'a>(
&'a self,
path: &'a Path,
) -> BoxedFuture<'a, Result<Box<dyn Reader + 'a>, AssetReaderError>> {
Box::pin(async {
let reader = Self::read_meta(self, path).await?;
Ok(Box::new(reader) as Box<dyn Reader>)
})
}
fn read_directory<'a>(
&'a self,
path: &'a Path,
) -> BoxedFuture<'a, Result<Box<PathStream>, AssetReaderError>> {
Box::pin(Self::read_directory(self, path))
}
fn is_directory<'a>(
&'a self,
path: &'a Path,
) -> BoxedFuture<'a, Result<bool, AssetReaderError>> {
Box::pin(Self::is_directory(self, path))
}
fn read_meta_bytes<'a>(
&'a self,
path: &'a Path,
) -> BoxedFuture<'a, Result<Vec<u8>, AssetReaderError>> {
Box::pin(Self::read_meta_bytes(self, path))
}
}
pub type Writer = dyn AsyncWrite + Unpin + Send + Sync;
pub type PathStream = dyn Stream<Item = PathBuf> + Unpin + Send;
#[derive(Error, Debug)]
pub enum AssetWriterError {
#[error("encountered an io error while loading asset: {0}")]
Io(#[from] std::io::Error),
}
pub trait AssetWriter: Send + Sync + 'static {
fn write<'a>(
&'a self,
path: &'a Path,
) -> impl ConditionalSendFuture<Output = Result<Box<Writer>, AssetWriterError>>;
fn write_meta<'a>(
&'a self,
path: &'a Path,
) -> impl ConditionalSendFuture<Output = Result<Box<Writer>, AssetWriterError>>;
fn remove<'a>(
&'a self,
path: &'a Path,
) -> impl ConditionalSendFuture<Output = Result<(), AssetWriterError>>;
fn remove_meta<'a>(
&'a self,
path: &'a Path,
) -> impl ConditionalSendFuture<Output = Result<(), AssetWriterError>>;
fn rename<'a>(
&'a self,
old_path: &'a Path,
new_path: &'a Path,
) -> impl ConditionalSendFuture<Output = Result<(), AssetWriterError>>;
fn rename_meta<'a>(
&'a self,
old_path: &'a Path,
new_path: &'a Path,
) -> impl ConditionalSendFuture<Output = Result<(), AssetWriterError>>;
fn create_directory<'a>(
&'a self,
path: &'a Path,
) -> impl ConditionalSendFuture<Output = Result<(), AssetWriterError>>;
fn remove_directory<'a>(
&'a self,
path: &'a Path,
) -> impl ConditionalSendFuture<Output = Result<(), AssetWriterError>>;
fn remove_empty_directory<'a>(
&'a self,
path: &'a Path,
) -> impl ConditionalSendFuture<Output = Result<(), AssetWriterError>>;
fn remove_assets_in_directory<'a>(
&'a self,
path: &'a Path,
) -> impl ConditionalSendFuture<Output = Result<(), AssetWriterError>>;
fn write_bytes<'a>(
&'a self,
path: &'a Path,
bytes: &'a [u8],
) -> impl ConditionalSendFuture<Output = Result<(), AssetWriterError>> {
async {
let mut writer = self.write(path).await?;
writer.write_all(bytes).await?;
writer.flush().await?;
Ok(())
}
}
fn write_meta_bytes<'a>(
&'a self,
path: &'a Path,
bytes: &'a [u8],
) -> impl ConditionalSendFuture<Output = Result<(), AssetWriterError>> {
async {
let mut meta_writer = self.write_meta(path).await?;
meta_writer.write_all(bytes).await?;
meta_writer.flush().await?;
Ok(())
}
}
}
pub trait ErasedAssetWriter: Send + Sync + 'static {
fn write<'a>(
&'a self,
path: &'a Path,
) -> BoxedFuture<'a, Result<Box<Writer>, AssetWriterError>>;
fn write_meta<'a>(
&'a self,
path: &'a Path,
) -> BoxedFuture<'a, Result<Box<Writer>, AssetWriterError>>;
fn remove<'a>(&'a self, path: &'a Path) -> BoxedFuture<'a, Result<(), AssetWriterError>>;
fn remove_meta<'a>(&'a self, path: &'a Path) -> BoxedFuture<'a, Result<(), AssetWriterError>>;
fn rename<'a>(
&'a self,
old_path: &'a Path,
new_path: &'a Path,
) -> BoxedFuture<'a, Result<(), AssetWriterError>>;
fn rename_meta<'a>(
&'a self,
old_path: &'a Path,
new_path: &'a Path,
) -> BoxedFuture<'a, Result<(), AssetWriterError>>;
fn create_directory<'a>(
&'a self,
path: &'a Path,
) -> BoxedFuture<'a, Result<(), AssetWriterError>>;
fn remove_directory<'a>(
&'a self,
path: &'a Path,
) -> BoxedFuture<'a, Result<(), AssetWriterError>>;
fn remove_empty_directory<'a>(
&'a self,
path: &'a Path,
) -> BoxedFuture<'a, Result<(), AssetWriterError>>;
fn remove_assets_in_directory<'a>(
&'a self,
path: &'a Path,
) -> BoxedFuture<'a, Result<(), AssetWriterError>>;
fn write_bytes<'a>(
&'a self,
path: &'a Path,
bytes: &'a [u8],
) -> BoxedFuture<'a, Result<(), AssetWriterError>>;
fn write_meta_bytes<'a>(
&'a self,
path: &'a Path,
bytes: &'a [u8],
) -> BoxedFuture<'a, Result<(), AssetWriterError>>;
}
impl<T: AssetWriter> ErasedAssetWriter for T {
fn write<'a>(
&'a self,
path: &'a Path,
) -> BoxedFuture<'a, Result<Box<Writer>, AssetWriterError>> {
Box::pin(Self::write(self, path))
}
fn write_meta<'a>(
&'a self,
path: &'a Path,
) -> BoxedFuture<'a, Result<Box<Writer>, AssetWriterError>> {
Box::pin(Self::write_meta(self, path))
}
fn remove<'a>(&'a self, path: &'a Path) -> BoxedFuture<'a, Result<(), AssetWriterError>> {
Box::pin(Self::remove(self, path))
}
fn remove_meta<'a>(&'a self, path: &'a Path) -> BoxedFuture<'a, Result<(), AssetWriterError>> {
Box::pin(Self::remove_meta(self, path))
}
fn rename<'a>(
&'a self,
old_path: &'a Path,
new_path: &'a Path,
) -> BoxedFuture<'a, Result<(), AssetWriterError>> {
Box::pin(Self::rename(self, old_path, new_path))
}
fn rename_meta<'a>(
&'a self,
old_path: &'a Path,
new_path: &'a Path,
) -> BoxedFuture<'a, Result<(), AssetWriterError>> {
Box::pin(Self::rename_meta(self, old_path, new_path))
}
fn create_directory<'a>(
&'a self,
path: &'a Path,
) -> BoxedFuture<'a, Result<(), AssetWriterError>> {
Box::pin(Self::create_directory(self, path))
}
fn remove_directory<'a>(
&'a self,
path: &'a Path,
) -> BoxedFuture<'a, Result<(), AssetWriterError>> {
Box::pin(Self::remove_directory(self, path))
}
fn remove_empty_directory<'a>(
&'a self,
path: &'a Path,
) -> BoxedFuture<'a, Result<(), AssetWriterError>> {
Box::pin(Self::remove_empty_directory(self, path))
}
fn remove_assets_in_directory<'a>(
&'a self,
path: &'a Path,
) -> BoxedFuture<'a, Result<(), AssetWriterError>> {
Box::pin(Self::remove_assets_in_directory(self, path))
}
fn write_bytes<'a>(
&'a self,
path: &'a Path,
bytes: &'a [u8],
) -> BoxedFuture<'a, Result<(), AssetWriterError>> {
Box::pin(Self::write_bytes(self, path, bytes))
}
fn write_meta_bytes<'a>(
&'a self,
path: &'a Path,
bytes: &'a [u8],
) -> BoxedFuture<'a, Result<(), AssetWriterError>> {
Box::pin(Self::write_meta_bytes(self, path, bytes))
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum AssetSourceEvent {
AddedAsset(PathBuf),
ModifiedAsset(PathBuf),
RemovedAsset(PathBuf),
RenamedAsset { old: PathBuf, new: PathBuf },
AddedMeta(PathBuf),
ModifiedMeta(PathBuf),
RemovedMeta(PathBuf),
RenamedMeta { old: PathBuf, new: PathBuf },
AddedFolder(PathBuf),
RemovedFolder(PathBuf),
RenamedFolder { old: PathBuf, new: PathBuf },
RemovedUnknown {
path: PathBuf,
is_meta: bool,
},
}
pub trait AssetWatcher: Send + Sync + 'static {}
pub struct VecReader {
pub bytes: Vec<u8>,
bytes_read: usize,
}
impl VecReader {
pub fn new(bytes: Vec<u8>) -> Self {
Self {
bytes_read: 0,
bytes,
}
}
}
impl AsyncRead for VecReader {
fn poll_read(
self: Pin<&mut Self>,
_cx: &mut Context<'_>,
buf: &mut [u8],
) -> Poll<futures_io::Result<usize>> {
let this = self.get_mut();
Poll::Ready(Ok(slice_read(&this.bytes, &mut this.bytes_read, buf)))
}
}
impl AsyncSeek for VecReader {
fn poll_seek(
self: Pin<&mut Self>,
_cx: &mut Context<'_>,
pos: SeekFrom,
) -> Poll<std::io::Result<u64>> {
let this = self.get_mut();
Poll::Ready(slice_seek(&this.bytes, &mut this.bytes_read, pos))
}
}
impl Reader for VecReader {
fn read_to_end<'a>(
&'a mut self,
buf: &'a mut Vec<u8>,
) -> StackFuture<'a, std::io::Result<usize>, STACK_FUTURE_SIZE> {
read_to_end(&self.bytes, &mut self.bytes_read, buf)
}
fn seekable(&mut self) -> Result<&mut dyn SeekableReader, ReaderNotSeekableError> {
Ok(self)
}
}
pub struct SliceReader<'a> {
bytes: &'a [u8],
bytes_read: usize,
}
impl<'a> SliceReader<'a> {
pub fn new(bytes: &'a [u8]) -> Self {
Self {
bytes,
bytes_read: 0,
}
}
}
impl<'a> AsyncRead for SliceReader<'a> {
fn poll_read(
mut self: Pin<&mut Self>,
_cx: &mut Context<'_>,
buf: &mut [u8],
) -> Poll<std::io::Result<usize>> {
Poll::Ready(Ok(slice_read(self.bytes, &mut self.bytes_read, buf)))
}
}
impl<'a> AsyncSeek for SliceReader<'a> {
fn poll_seek(
mut self: Pin<&mut Self>,
_cx: &mut Context<'_>,
pos: SeekFrom,
) -> Poll<std::io::Result<u64>> {
Poll::Ready(slice_seek(self.bytes, &mut self.bytes_read, pos))
}
}
impl Reader for SliceReader<'_> {
fn read_to_end<'a>(
&'a mut self,
buf: &'a mut Vec<u8>,
) -> StackFuture<'a, std::io::Result<usize>, STACK_FUTURE_SIZE> {
read_to_end(self.bytes, &mut self.bytes_read, buf)
}
fn seekable(&mut self) -> Result<&mut dyn SeekableReader, ReaderNotSeekableError> {
Ok(self)
}
}
pub(crate) fn slice_read(slice: &[u8], bytes_read: &mut usize, buf: &mut [u8]) -> usize {
if *bytes_read >= slice.len() {
0
} else {
let n = std::io::Read::read(&mut &slice[(*bytes_read)..], buf).unwrap();
*bytes_read += n;
n
}
}
pub(crate) fn slice_seek(
slice: &[u8],
bytes_read: &mut usize,
pos: SeekFrom,
) -> std::io::Result<u64> {
let make_error = || {
Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"seek position is out of range",
))
};
let (origin, offset) = match pos {
SeekFrom::Current(offset) => (*bytes_read, Ok(offset)),
SeekFrom::Start(offset) => (0, offset.try_into()),
SeekFrom::End(offset) => (slice.len(), Ok(offset)),
};
let Ok(offset) = offset else {
return make_error();
};
let Ok(origin): Result<i64, _> = origin.try_into() else {
return make_error();
};
let Ok(new_pos) = (origin + offset).try_into() else {
return make_error();
};
*bytes_read = new_pos;
Ok(new_pos as _)
}
pub(crate) fn read_to_end<'a>(
source: &'a [u8],
bytes_read: &'a mut usize,
dest: &'a mut Vec<u8>,
) -> StackFuture<'a, std::io::Result<usize>, STACK_FUTURE_SIZE> {
StackFuture::from(async {
if *bytes_read >= source.len() {
Ok(0)
} else {
dest.extend_from_slice(&source[*bytes_read..]);
let n = source.len() - *bytes_read;
*bytes_read = source.len();
Ok(n)
}
})
}
pub(crate) fn get_meta_path(path: &Path) -> PathBuf {
let mut meta_path = path.to_path_buf();
let mut extension = path.extension().unwrap_or_default().to_os_string();
if !extension.is_empty() {
extension.push(".");
}
extension.push("meta");
meta_path.set_extension(extension);
meta_path
}
#[cfg(any(target_arch = "wasm32", target_os = "android"))]
struct EmptyPathStream;
#[cfg(any(target_arch = "wasm32", target_os = "android"))]
impl Stream for EmptyPathStream {
type Item = PathBuf;
fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
Poll::Ready(None)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn get_meta_path_no_extension() {
assert_eq!(
get_meta_path(Path::new("foo")).to_str().unwrap(),
"foo.meta"
);
}
#[test]
fn get_meta_path_with_extension() {
assert_eq!(
get_meta_path(Path::new("foo.bar")).to_str().unwrap(),
"foo.bar.meta"
);
}
}