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#[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 #[must_use]
65 pub(crate) fn new() -> Self {
66 Self {
67 loaded: HashMap::default(),
68 fallback: None,
69 }
70 }
71
72 #[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 #[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 pub(crate) fn add_asset(&mut self, path: &'static Path, data: &'static [u8]) {
102 self.loaded.insert(path, data);
103 }
104
105 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#[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<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}