Skip to main content

ferroui_base/platform/
standard_asset_loader.rs

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
9/// Loads assets compiled into the application binary.
10///
11/// The type is considered unstable; use the functions of
12/// [`AssetLoader`](super::AssetLoader) instead.
13pub 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    /// Creates an asset loader. `assembly` is the default assembly from
36    /// which to load embedded-resource (`resm:`) assets for which no
37    /// assembly is specified.
38    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    // --- not from upstream ---
269
270    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        // Paths are unescaped, the crate name is matched ignoring case.
293        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        // Relative URIs resolve against the base URI.
300        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        // Without an assembly in the query, the assembly of the base URI and
358        // then the default assembly are used.
359        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        // A crate that registered only embedded resources has no assets of
379        // the asset scheme.
380        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}