1use super::internal::{
2 AssemblyDescriptor, AssemblyDescriptorResolver, IAssemblyDescriptor, IAssemblyDescriptorResolver, IAssetDescriptor,
3};
4use super::{AssetAssembly, AssetStream, IAssetLoader, ASSET_SCHEME};
5use crate::utilities::{Uri, UriExtensions};
6use std::cell::RefCell;
7use std::rc::Rc;
8
9pub struct StandardAssetLoader {
14 assembly_descriptor_resolver: Box<dyn IAssemblyDescriptorResolver>,
15 default_resm_assembly: RefCell<Option<Rc<dyn IAssemblyDescriptor>>>,
16}
17
18impl Default for StandardAssetLoader {
19 fn default() -> Self {
20 Self::new(None)
21 }
22}
23
24impl StandardAssetLoader {
25 pub(crate) fn with_resolver(
26 resolver: Box<dyn IAssemblyDescriptorResolver>,
27 assembly: Option<&AssetAssembly>,
28 ) -> Self {
29 Self {
30 assembly_descriptor_resolver: resolver,
31 default_resm_assembly: RefCell::new(assembly.map(Self::describe)),
32 }
33 }
34
35 pub fn new(assembly: Option<&AssetAssembly>) -> Self {
39 Self::with_resolver(Box::new(AssemblyDescriptorResolver::new()), assembly)
40 }
41
42 fn describe(assembly: &AssetAssembly) -> Rc<dyn IAssemblyDescriptor> {
43 Rc::new(AssemblyDescriptor::new(assembly))
44 }
45
46 fn not_found(uri: &Uri) -> std::io::Error {
47 std::io::Error::new(std::io::ErrorKind::NotFound, format!("The resource {uri} could not be found."))
48 }
49
50 fn try_get_asset(&self, uri: &Uri, base_uri: Option<&Uri>) -> Option<Rc<dyn IAssetDescriptor>> {
51 if UriExtensions::is_absolute_resm(uri) {
52 let assembly = self
53 .try_get_assembly(Some(uri))
54 .or_else(|| self.try_get_assembly(base_uri))
55 .or_else(|| self.default_resm_assembly.borrow().clone());
56
57 if let Some(resources) = assembly.as_ref().and_then(|assembly| assembly.resources()) {
58 let resource_key = uri.absolute_path();
59
60 if let Some(asset_descriptor) = resources.get(resource_key) {
61 return Some(asset_descriptor);
62 }
63 }
64 }
65
66 let uri = UriExtensions::ensure_absolute(uri, base_uri).ok()?;
67
68 if UriExtensions::is_asset(&uri) {
69 if let Some((assembly, path)) = self.try_get_res_asm_and_path(&uri) {
70 let resources = assembly.ferro_resources()?;
71
72 if let Some(asset_descriptor) = resources.get(&path) {
73 return Some(asset_descriptor);
74 }
75 }
76 }
77
78 None
79 }
80
81 fn try_get_res_asm_and_path(&self, uri: &Uri) -> Option<(Rc<dyn IAssemblyDescriptor>, String)> {
82 let path = UriExtensions::get_unescape_absolute_path(uri);
83
84 self.try_load_assembly(UriExtensions::authority(uri)).map(|assembly| (assembly, path))
85 }
86
87 fn try_get_assembly(&self, uri: Option<&Uri>) -> Option<Rc<dyn IAssemblyDescriptor>> {
88 let uri = uri?;
89
90 if !uri.is_absolute_uri() {
91 return None;
92 }
93
94 if UriExtensions::is_asset(uri) {
95 if let Some((assembly, _)) = self.try_get_res_asm_and_path(uri) {
96 return Some(assembly);
97 }
98 }
99
100 if UriExtensions::is_resm(uri) {
101 let assembly_name = UriExtensions::get_assembly_name_from_query(uri);
102
103 if !assembly_name.is_empty() {
104 if let Some(assembly) = self.try_load_assembly(&assembly_name) {
105 return Some(assembly);
106 }
107 }
108 }
109
110 None
111 }
112
113 fn try_load_assembly(&self, assembly_name: &str) -> Option<Rc<dyn IAssemblyDescriptor>> {
114 self.assembly_descriptor_resolver.get_assembly(assembly_name).ok()
115 }
116}
117
118impl IAssetLoader for StandardAssetLoader {
119 fn set_default_assembly(&self, assembly: &AssetAssembly) {
120 *self.default_resm_assembly.borrow_mut() = Some(Self::describe(assembly));
121 }
122
123 fn exists(&self, uri: &Uri, base_uri: Option<&Uri>) -> bool {
124 self.try_get_asset(uri, base_uri).is_some()
125 }
126
127 fn open(&self, uri: &Uri, base_uri: Option<&Uri>) -> std::io::Result<Box<dyn AssetStream>> {
128 Ok(self.open_and_get_assembly(uri, base_uri)?.0)
129 }
130
131 fn open_and_get_assembly(
132 &self,
133 uri: &Uri,
134 base_uri: Option<&Uri>,
135 ) -> std::io::Result<(Box<dyn AssetStream>, AssetAssembly)> {
136 match self.try_get_asset(uri, base_uri) {
137 Some(asset_descriptor) => Ok((asset_descriptor.get_stream(), asset_descriptor.assembly().clone())),
138 None => Err(Self::not_found(uri)),
139 }
140 }
141
142 fn get_assembly(&self, uri: &Uri, base_uri: Option<&Uri>) -> Option<AssetAssembly> {
143 let uri = match base_uri {
144 Some(base_uri) if !uri.is_absolute_uri() => {
145 if !base_uri.is_absolute_uri() {
146 return None;
147 }
148 Uri::combine(base_uri, uri)
149 }
150 _ => uri.clone(),
151 };
152 self.try_get_assembly(Some(&uri)).map(|descriptor| descriptor.assembly().clone())
153 }
154
155 fn get_assets(&self, uri: &Uri, base_uri: Option<&Uri>) -> Vec<Uri> {
156 if UriExtensions::is_absolute_resm(uri) {
157 let assembly = self.try_get_assembly(Some(uri)).or_else(|| self.default_resm_assembly.borrow().clone());
158
159 let Some(assembly) = assembly else { return Vec::new() };
160 let Some(resources) = assembly.resources() else { return Vec::new() };
161 let path = UriExtensions::get_unescape_absolute_path(uri);
162 let name = assembly.name().unwrap_or_default();
163 return resources
164 .keys()
165 .filter(|key| key.contains(&path))
166 .filter_map(|key| Uri::absolute(&format!("resm:{key}?assembly={name}")).ok())
167 .collect();
168 }
169
170 let Ok(uri) = UriExtensions::ensure_absolute(uri, base_uri) else { return Vec::new() };
171 if UriExtensions::is_asset(&uri) {
172 let Some((assembly, mut path)) = self.try_get_res_asm_and_path(&uri) else { return Vec::new() };
173 let Some(resources) = assembly.ferro_resources() else { return Vec::new() };
174
175 if !path.is_empty() && !path.ends_with('/') {
176 path.push('/');
177 }
178
179 let name = assembly.name().unwrap_or_default();
180 return resources
181 .keys()
182 .filter(|key| key.starts_with(&path))
183 .filter_map(|key| Uri::absolute(&format!("{ASSET_SCHEME}://{name}{key}")).ok())
184 .collect();
185 }
186
187 Vec::new()
188 }
189
190 fn invalidate_assembly_cache(&self, name: &str) {
191 self.assembly_descriptor_resolver.invalidate_assembly_cache(name);
192 }
193
194 fn invalidate_assembly_cache_all(&self) {
195 self.assembly_descriptor_resolver.invalidate_assembly_cache_all();
196 }
197}
198
199#[cfg(test)]
200mod tests {
201 use super::super::internal::AssetMap;
202 use super::*;
203 use crate::platform::{register_assets, register_manifest_resources};
204 use crate::utilities::UriKind;
205 use std::collections::HashMap;
206 use std::io::Read;
207
208 const ASSEMBLY_NAME_WITH_WHITESPACE: &str = "Awesome Library";
209 const ASSEMBLY_NAME_WITH_NON_ASCII: &str = "Какое-то-название";
210
211 struct MockAssemblyDescriptor(AssetAssembly);
212
213 impl IAssemblyDescriptor for MockAssemblyDescriptor {
214 fn assembly(&self) -> &AssetAssembly {
215 &self.0
216 }
217 fn resources(&self) -> Option<&AssetMap> {
218 None
219 }
220 fn ferro_resources(&self) -> Option<&AssetMap> {
221 None
222 }
223 fn name(&self) -> Option<&str> {
224 None
225 }
226 }
227
228 struct MockResolver(HashMap<String, Rc<dyn IAssemblyDescriptor>>);
229
230 impl IAssemblyDescriptorResolver for MockResolver {
231 fn get_assembly(&self, name: &str) -> Result<Rc<dyn IAssemblyDescriptor>, String> {
232 self.0.get(name).cloned().ok_or_else(|| format!("unknown assembly {name}"))
233 }
234 fn invalidate_assembly_cache(&self, _name: &str) {}
235 fn invalidate_assembly_cache_all(&self) {}
236 }
237
238 fn resolver() -> Box<dyn IAssemblyDescriptorResolver> {
239 let mut assemblies: HashMap<String, Rc<dyn IAssemblyDescriptor>> = HashMap::new();
240 for name in [ASSEMBLY_NAME_WITH_WHITESPACE, ASSEMBLY_NAME_WITH_NON_ASCII] {
241 assemblies.insert(name.to_owned(), Rc::new(MockAssemblyDescriptor(AssetAssembly::new(name))));
242 }
243 Box::new(MockResolver(assemblies))
244 }
245
246 #[test]
247 fn assembly_name_with_whitespace_should_load_resm() {
248 let uri =
249 Uri::absolute(&format!("resm:Ferro.Base.UnitTests.Assets.something?assembly={ASSEMBLY_NAME_WITH_WHITESPACE}"))
250 .unwrap();
251 let loader = StandardAssetLoader::with_resolver(resolver(), None);
252
253 let assembly_actual = loader.get_assembly(&uri, None);
254
255 assert_eq!(Some(ASSEMBLY_NAME_WITH_WHITESPACE), assembly_actual.as_ref().map(AssetAssembly::name));
256 }
257
258 #[test]
259 fn invalid_assembly_name_should_yield_empty_enumerable() {
260 let uri = Uri::absolute(&format!("{ASSET_SCHEME}://InvalidAssembly")).unwrap();
261 let loader = StandardAssetLoader::with_resolver(resolver(), None);
262
263 let assembly_actual = loader.get_assets(&uri, None);
264
265 assert!(assembly_actual.is_empty());
266 }
267
268 fn read(mut stream: Box<dyn AssetStream>) -> Vec<u8> {
271 let mut content = Vec::new();
272 stream.read_to_end(&mut content).unwrap();
273 content
274 }
275
276 fn asset(path: &str) -> Uri {
277 Uri::absolute(&format!("{ASSET_SCHEME}://{path}")).unwrap()
278 }
279
280 #[test]
281 fn registered_assets_are_found_by_uri() {
282 register_assets(
283 "asset-loader-test-app",
284 &[("Assets/logo.png", b"logo"), ("/Assets/Icons/a b.svg", b"icon"), ("Styles/Main.xaml", b"xaml")],
285 );
286 let loader = StandardAssetLoader::new(None);
287
288 let logo = asset("asset-loader-test-app/Assets/logo.png");
289 assert!(loader.exists(&logo, None));
290 assert_eq!(b"logo".to_vec(), read(loader.open(&logo, None).unwrap()));
291
292 let icon = asset("Asset-Loader-Test-App/Assets/Icons/a%20b.svg");
294 let (stream, assembly) = loader.open_and_get_assembly(&icon, None).unwrap();
295 assert_eq!(b"icon".to_vec(), read(stream));
296 assert_eq!("asset-loader-test-app", assembly.name());
297 assert_eq!(Some(assembly), loader.get_assembly(&icon, None));
298
299 let base = asset("asset-loader-test-app/Styles/Main.xaml");
301 let relative = Uri::new("../Assets/logo.png", UriKind::Relative).unwrap();
302 assert!(loader.exists(&relative, Some(&base)));
303 assert!(!loader.exists(&relative, None));
304 let rooted = Uri::new("/Assets/logo.png", UriKind::Relative).unwrap();
305 assert_eq!(b"logo".to_vec(), read(loader.open(&rooted, Some(&base)).unwrap()));
306 assert_eq!("asset-loader-test-app", loader.get_assembly(&rooted, Some(&base)).unwrap().name());
307
308 let missing = asset("asset-loader-test-app/Assets/missing.png");
309 assert!(!loader.exists(&missing, None));
310 assert_eq!(std::io::ErrorKind::NotFound, loader.open(&missing, None).err().unwrap().kind());
311 assert!(!loader.exists(&asset("asset-loader-unknown-crate/Assets/logo.png"), None));
312 assert!(loader.open(&relative, None).is_err());
313 }
314
315 #[test]
316 fn get_assets_lists_a_folder() {
317 register_assets(
318 "asset-loader-folder-app",
319 &[("Assets/a.png", b"a"), ("Assets/Sub/b.png", b"b"), ("AssetsOther/c.png", b"c"), ("d.png", b"d")],
320 );
321 let loader = StandardAssetLoader::new(None);
322
323 let list = |uri: &str| -> Vec<String> {
324 loader.get_assets(&asset(uri), None).iter().map(|uri| uri.absolute_uri().to_owned()).collect()
325 };
326
327 assert_eq!(
328 vec![
329 format!("{ASSET_SCHEME}://asset-loader-folder-app/Assets/a.png"),
330 format!("{ASSET_SCHEME}://asset-loader-folder-app/Assets/Sub/b.png"),
331 ],
332 list("asset-loader-folder-app/Assets")
333 );
334 assert_eq!(4, list("asset-loader-folder-app").len());
335 assert_eq!(4, list("asset-loader-folder-app/").len());
336 assert!(list("asset-loader-folder-app/Missing").is_empty());
337
338 let relative = Uri::new("Assets/Sub", UriKind::Relative).unwrap();
339 let base = asset("asset-loader-folder-app/d.png");
340 assert_eq!(1, loader.get_assets(&relative, Some(&base)).len());
341 assert!(loader.get_assets(&relative, None).is_empty());
342 }
343
344 #[test]
345 fn manifest_resources_are_found_by_name() {
346 register_manifest_resources(
347 "asset-loader-resm-lib",
348 &[("Lib.Assets.one.txt", b"one"), ("Lib.Assets.two.txt", b"two"), ("Lib.Other.txt", b"other")],
349 );
350 let lib = AssetAssembly::new("asset-loader-resm-lib");
351 let loader = StandardAssetLoader::new(None);
352
353 let qualified = Uri::absolute("resm:Lib.Assets.one.txt?assembly=asset-loader-resm-lib").unwrap();
354 assert_eq!(b"one".to_vec(), read(loader.open(&qualified, None).unwrap()));
355 assert_eq!(Some(lib.clone()), loader.get_assembly(&qualified, None));
356
357 let unqualified = Uri::absolute("resm:Lib.Assets.two.txt").unwrap();
360 assert!(!loader.exists(&unqualified, None));
361 assert!(loader.exists(&unqualified, Some(&qualified)));
362 loader.set_default_assembly(&lib);
363 assert_eq!(b"two".to_vec(), read(loader.open(&unqualified, None).unwrap()));
364
365 let found: Vec<String> = loader
366 .get_assets(&Uri::absolute("resm:Lib.Assets").unwrap(), None)
367 .iter()
368 .map(|uri| uri.absolute_uri().to_owned())
369 .collect();
370 assert_eq!(
371 vec![
372 "resm:Lib.Assets.one.txt?assembly=asset-loader-resm-lib".to_owned(),
373 "resm:Lib.Assets.two.txt?assembly=asset-loader-resm-lib".to_owned(),
374 ],
375 found
376 );
377
378 assert!(loader.get_assets(&asset("asset-loader-resm-lib/"), None).is_empty());
381 }
382
383 #[test]
384 fn assembly_cache_is_invalidated_explicitly() {
385 register_assets("asset-loader-cache-app", &[("a.txt", b"1")]);
386 let loader = StandardAssetLoader::new(None);
387 let a = asset("asset-loader-cache-app/a.txt");
388 let b = asset("asset-loader-cache-app/b.txt");
389 assert!(loader.exists(&a, None));
390 assert!(!loader.exists(&b, None));
391
392 register_assets("asset-loader-cache-app", &[("b.txt", b"2"), ("a.txt", b"3")]);
393 assert!(!loader.exists(&b, None));
394 loader.invalidate_assembly_cache("asset-loader-cache-app");
395 assert!(loader.exists(&b, None));
396 assert_eq!(b"3".to_vec(), read(loader.open(&a, None).unwrap()));
397
398 register_assets("asset-loader-cache-app", &[("c.txt", b"4")]);
399 loader.invalidate_assembly_cache_all();
400 assert!(loader.exists(&asset("asset-loader-cache-app/c.txt"), None));
401 }
402
403 #[test]
404 fn shortest_matching_crate_name_wins() {
405 register_assets("asset-loader-prefix-long", &[("x", b"long")]);
406 register_assets("asset-loader-prefix", &[("x", b"short")]);
407 let loader = StandardAssetLoader::new(None);
408 assert_eq!(b"short".to_vec(), read(loader.open(&asset("asset-loader-prefix/x"), None).unwrap()));
409 assert_eq!(b"long".to_vec(), read(loader.open(&asset("asset-loader-prefix-long/x"), None).unwrap()));
410 }
411}