bevy_embedded_assets/
asset_reader.rs

1use std::{
2    collections::HashMap,
3    io::Read,
4    path::{Path, PathBuf},
5    pin::Pin,
6    task::Poll,
7};
8
9use bevy_asset::io::{
10    AssetReader, AssetReaderError, AsyncSeekForward, ErasedAssetReader, PathStream, Reader,
11};
12use futures_io::{AsyncRead, AsyncSeek};
13use futures_lite::Stream;
14use thiserror::Error;
15
16use crate::{EmbeddedRegistry, include_all_assets};
17
18/// Struct which can be used to retrieve embedded assets directly
19/// without the normal Bevy `Handle<T>` approach.  This is useful
20/// for cases where you need an asset outside the Bevy ECS environment.
21///
22/// This is only available when the `default-source` cargo feature is enabled.
23///
24/// Example usage is below which assumes you have an asset named `image.png`
25/// in your `assets` folder (which this crate embeds at compile time).
26/// ```rust
27/// use bevy_embedded_assets::{DataReader, EmbeddedAssetReader};
28/// use std::path::Path;
29///
30/// fn some_bevy_system() {
31///     let embedded: EmbeddedAssetReader = EmbeddedAssetReader::preloaded();
32///     let reader: DataReader = embedded.load_path_sync(&Path::new("image.png")).unwrap();
33///     let image_data: Vec<u8> = reader.0.to_vec();
34///     // Do what you need with the data
35/// }
36/// ```
37#[allow(clippy::module_name_repetitions)]
38pub struct EmbeddedAssetReader {
39    loaded: HashMap<&'static Path, &'static [u8]>,
40    fallback: Option<Box<dyn ErasedAssetReader>>,
41}
42
43impl std::fmt::Debug for EmbeddedAssetReader {
44    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45        f.debug_struct("EmbeddedAssetReader")
46            .finish_non_exhaustive()
47    }
48}
49
50impl Default for EmbeddedAssetReader {
51    fn default() -> Self {
52        Self::new()
53    }
54}
55
56impl EmbeddedRegistry for &mut EmbeddedAssetReader {
57    fn insert_included_asset(&mut self, name: &'static str, bytes: &'static [u8]) {
58        self.add_asset(Path::new(name), bytes);
59    }
60}
61
62impl EmbeddedAssetReader {
63    /// Create an empty [`EmbeddedAssetReader`].
64    #[must_use]
65    pub(crate) fn new() -> Self {
66        Self {
67            loaded: HashMap::default(),
68            fallback: None,
69        }
70    }
71
72    /// Create an [`EmbeddedAssetReader`] loaded with all the assets found by the build script.
73    ///
74    /// This ensures the [`EmbeddedAssetReader`] has all (embedded) assets loaded and can be used
75    /// directly without the typical Bevy `Handle<T>` approach.  Retrieve assets directly after
76    /// calling `preloaded` with [`EmbeddedAssetReader::load_path_sync()`].
77    #[must_use]
78    pub fn preloaded() -> Self {
79        let mut new = Self {
80            loaded: HashMap::default(),
81            fallback: None,
82        };
83        include_all_assets(&mut new);
84        new
85    }
86
87    /// Create an [`EmbeddedAssetReader`] loaded with all the assets found by the build script.
88    #[must_use]
89    pub(crate) fn preloaded_with_default(
90        mut default: impl FnMut() -> Box<dyn ErasedAssetReader> + Send + Sync + 'static,
91    ) -> Self {
92        let mut new = Self {
93            loaded: HashMap::default(),
94            fallback: Some(default()),
95        };
96        include_all_assets(&mut new);
97        new
98    }
99
100    /// Add an asset to this [`EmbeddedAssetReader`].
101    pub(crate) fn add_asset(&mut self, path: &'static Path, data: &'static [u8]) {
102        self.loaded.insert(path, data);
103    }
104
105    /// Get the data from the asset matching the path provided.
106    ///
107    /// # Errors
108    ///
109    /// This will returns an error if the path is not known.
110    pub fn load_path_sync(&self, path: &Path) -> Result<DataReader, AssetReaderError> {
111        self.loaded
112            .get(path)
113            .map(|b| DataReader(b))
114            .ok_or_else(|| AssetReaderError::NotFound(path.to_path_buf()))
115    }
116
117    fn has_file_sync(&self, path: &Path) -> bool {
118        self.loaded.contains_key(path)
119    }
120
121    fn is_directory_sync(&self, path: &Path) -> bool {
122        let as_folder = path.join("");
123        self.loaded
124            .keys()
125            .any(|loaded_path| loaded_path.starts_with(&as_folder) && loaded_path != &path)
126    }
127
128    fn read_directory_sync(&self, path: &Path) -> Result<DirReader, AssetReaderError> {
129        if self.is_directory_sync(path) {
130            let paths: Vec<_> = self
131                .loaded
132                .keys()
133                .filter(|loaded_path| loaded_path.starts_with(path))
134                .map(|t| t.to_path_buf())
135                .collect();
136            Ok(DirReader(paths))
137        } else {
138            Err(AssetReaderError::NotFound(path.to_path_buf()))
139        }
140    }
141}
142
143/// A wrapper around the raw bytes of an asset.
144/// This is returned by [`EmbeddedAssetReader::load_path_sync()`].
145///
146/// To get the raw data, use `reader.0`.
147#[derive(Default, Debug, Clone, Copy)]
148pub struct DataReader(pub &'static [u8]);
149
150impl Reader for DataReader {
151    fn read_to_end<'a>(
152        &'a mut self,
153        buf: &'a mut Vec<u8>,
154    ) -> bevy_asset::io::StackFuture<
155        'a,
156        std::io::Result<usize>,
157        { bevy_asset::io::STACK_FUTURE_SIZE },
158    > {
159        let future = futures_lite::AsyncReadExt::read_to_end(self, buf);
160        bevy_asset::io::StackFuture::from(future)
161    }
162}
163
164impl AsyncRead for DataReader {
165    fn poll_read(
166        self: Pin<&mut Self>,
167        _: &mut std::task::Context<'_>,
168        buf: &mut [u8],
169    ) -> Poll<futures_io::Result<usize>> {
170        let read = self.get_mut().0.read(buf);
171        Poll::Ready(read)
172    }
173}
174
175impl AsyncSeek for DataReader {
176    fn poll_seek(
177        self: Pin<&mut Self>,
178        _: &mut std::task::Context<'_>,
179        _pos: futures_io::SeekFrom,
180    ) -> Poll<futures_io::Result<u64>> {
181        Poll::Ready(Err(futures_io::Error::new(
182            futures_io::ErrorKind::Other,
183            EmbeddedDataReaderError::SeekNotSupported,
184        )))
185    }
186}
187
188impl AsyncSeekForward for DataReader {
189    fn poll_seek_forward(
190        self: Pin<&mut Self>,
191        _: &mut std::task::Context<'_>,
192        _offset: u64,
193    ) -> Poll<futures_io::Result<u64>> {
194        Poll::Ready(Err(futures_io::Error::new(
195            futures_io::ErrorKind::Other,
196            EmbeddedDataReaderError::SeekNotSupported,
197        )))
198    }
199}
200
201#[derive(Error, Debug)]
202enum EmbeddedDataReaderError {
203    #[error("Seek is not supported when embeded")]
204    SeekNotSupported,
205}
206
207struct DirReader(Vec<PathBuf>);
208
209impl Stream for DirReader {
210    type Item = PathBuf;
211
212    fn poll_next(
213        self: Pin<&mut Self>,
214        _cx: &mut std::task::Context<'_>,
215    ) -> Poll<Option<Self::Item>> {
216        let this = self.get_mut();
217        Poll::Ready(this.0.pop())
218    }
219}
220
221pub(crate) fn get_meta_path(path: &Path) -> PathBuf {
222    let mut meta_path = path.to_path_buf();
223    let mut extension = path
224        .extension()
225        .expect("asset paths must have extensions")
226        .to_os_string();
227    extension.push(".meta");
228    meta_path.set_extension(extension);
229    meta_path
230}
231
232impl AssetReader for EmbeddedAssetReader {
233    // async fn read<'a>(&'a self, path: &'a Path) -> Result<Box<dyn Reader>, AssetReaderError> {
234    async fn read<'a>(&'a self, path: &'a Path) -> Result<impl Reader + 'a, AssetReaderError> {
235        if self.has_file_sync(path) {
236            self.load_path_sync(path).map(|reader| {
237                let boxed: Box<dyn Reader> = Box::new(reader);
238                boxed
239            })
240        } else if let Some(fallback) = self.fallback.as_ref() {
241            fallback.read(path).await
242        } else {
243            Err(AssetReaderError::NotFound(path.to_path_buf()))
244        }
245    }
246
247    async fn read_meta<'a>(&'a self, path: &'a Path) -> Result<impl Reader + 'a, AssetReaderError> {
248        let meta_path = get_meta_path(path);
249        if self.has_file_sync(&meta_path) {
250            self.load_path_sync(&meta_path).map(|reader| {
251                let boxed: Box<dyn Reader> = Box::new(reader);
252                boxed
253            })
254        } else if let Some(fallback) = self.fallback.as_ref() {
255            fallback.read_meta(path).await
256        } else {
257            Err(AssetReaderError::NotFound(meta_path))
258        }
259    }
260
261    async fn read_directory<'a>(
262        &'a self,
263        path: &'a Path,
264    ) -> Result<Box<PathStream>, AssetReaderError> {
265        self.read_directory_sync(path).map(|read_dir| {
266            let boxed: Box<PathStream> = Box::new(read_dir);
267            boxed
268        })
269    }
270
271    async fn is_directory<'a>(&'a self, path: &'a Path) -> Result<bool, AssetReaderError> {
272        Ok(self.is_directory_sync(path))
273    }
274}
275
276#[cfg(test)]
277mod tests {
278    use std::path::Path;
279
280    use crate::asset_reader::EmbeddedAssetReader;
281
282    #[cfg_attr(not(target_arch = "wasm32"), test)]
283    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
284    fn load_path() {
285        let mut embedded = EmbeddedAssetReader::new();
286        embedded.add_asset(Path::new("asset.png"), &[1, 2, 3]);
287        embedded.add_asset(Path::new("other_asset.png"), &[4, 5, 6]);
288        assert!(embedded.load_path_sync(&Path::new("asset.png")).is_ok());
289        assert_eq!(
290            embedded.load_path_sync(&Path::new("asset.png")).unwrap().0,
291            [1, 2, 3]
292        );
293        assert_eq!(
294            embedded
295                .load_path_sync(&Path::new("other_asset.png"))
296                .unwrap()
297                .0,
298            [4, 5, 6]
299        );
300        assert!(embedded.load_path_sync(&Path::new("asset")).is_err());
301        assert!(embedded.load_path_sync(&Path::new("other")).is_err());
302    }
303
304    #[cfg_attr(not(target_arch = "wasm32"), test)]
305    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
306    fn is_directory() {
307        let mut embedded = EmbeddedAssetReader::new();
308        embedded.add_asset(Path::new("asset.png"), &[]);
309        embedded.add_asset(Path::new("directory/asset.png"), &[]);
310        assert!(!embedded.is_directory_sync(&Path::new("asset.png")));
311        assert!(!embedded.is_directory_sync(&Path::new("asset")));
312        assert!(embedded.is_directory_sync(&Path::new("directory")));
313        assert!(embedded.is_directory_sync(&Path::new("directory/")));
314        assert!(!embedded.is_directory_sync(&Path::new("directory/asset")));
315    }
316
317    #[cfg_attr(not(target_arch = "wasm32"), test)]
318    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
319    fn read_directory() {
320        let mut embedded = EmbeddedAssetReader::new();
321        embedded.add_asset(Path::new("asset.png"), &[]);
322        embedded.add_asset(Path::new("directory/asset.png"), &[]);
323        embedded.add_asset(Path::new("directory/asset2.png"), &[]);
324        assert!(
325            embedded
326                .read_directory_sync(&Path::new("asset.png"))
327                .is_err()
328        );
329        assert!(
330            embedded
331                .read_directory_sync(&Path::new("directory"))
332                .is_ok()
333        );
334        let mut list = embedded
335            .read_directory_sync(&Path::new("directory"))
336            .unwrap()
337            .0
338            .iter()
339            .map(|p| p.to_string_lossy().to_string())
340            .collect::<Vec<_>>();
341        list.sort();
342        assert_eq!(list, vec!["directory/asset.png", "directory/asset2.png"]);
343    }
344
345    #[cfg(target_arch = "wasm32")]
346    use wasm_bindgen_test::wasm_bindgen_test;
347
348    #[cfg_attr(not(target_arch = "wasm32"), test)]
349    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
350    fn check_preloaded_simple() {
351        let embedded = EmbeddedAssetReader::preloaded();
352
353        let path = "example_asset.test";
354
355        let loaded = embedded.load_path_sync(&Path::new(path));
356        assert!(loaded.is_ok());
357        let raw_asset = loaded.unwrap();
358        assert!(String::from_utf8(raw_asset.0.to_vec()).is_ok());
359        assert_eq!(String::from_utf8(raw_asset.0.to_vec()).unwrap(), "hello");
360    }
361
362    #[cfg_attr(not(target_arch = "wasm32"), test)]
363    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
364    fn check_preloaded_special_chars() {
365        let embedded = EmbeddedAssetReader::preloaded();
366
367        let path = "açèt.test";
368
369        let loaded = embedded.load_path_sync(&Path::new(path));
370        assert!(loaded.is_ok());
371        let raw_asset = loaded.unwrap();
372        assert!(String::from_utf8(raw_asset.0.to_vec()).is_ok());
373        assert_eq!(
374            String::from_utf8(raw_asset.0.to_vec()).unwrap(),
375            "with special chars"
376        );
377    }
378
379    #[cfg_attr(not(target_arch = "wasm32"), test)]
380    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
381    fn check_preloaded_subdir() {
382        let embedded = EmbeddedAssetReader::preloaded();
383
384        let path = "subdir/other_asset.test";
385
386        let loaded = embedded.load_path_sync(&Path::new(path));
387        assert!(loaded.is_ok());
388        let raw_asset = loaded.unwrap();
389        assert!(String::from_utf8(raw_asset.0.to_vec()).is_ok());
390        assert_eq!(
391            String::from_utf8(raw_asset.0.to_vec()).unwrap(),
392            "in subdirectory"
393        );
394    }
395}