1use std::cell::RefCell;
2use std::collections::BTreeMap;
3use std::fmt::Write as _;
4use std::fs;
5use std::path::{Path, PathBuf};
6use std::rc::Rc;
7use std::sync::atomic::{AtomicBool, Ordering};
8use std::sync::mpsc::{Receiver, Sender, channel};
9use std::sync::{Arc, LazyLock};
10
11use gpui::{AppContext as _, Image, ImageCacheError, ImageFormat, ImageSource, RenderImage};
12use rhai::{CustomType, TypeBuilder};
13use thiserror::Error;
14
15use crate::{
16 AsyncDelivery, AsyncScope, ComponentInstancePath, OpaqueHandle, Rgba8, ScriptCallback,
17 ScriptGeneration, UiValue,
18};
19
20const MAX_SVG_RASTER_DIMENSION: u32 = 16_384;
21const MAX_SVG_RASTER_DIMENSION_F32: f32 = 16_384.0;
22const MAX_SVG_RASTER_PIXELS: u64 = 16_777_216;
23const SVG_VARIANT_CACHE_MAX_ENTRIES: usize = 256;
24const SVG_VARIANT_CACHE_MAX_BYTES: usize = 128 * 1024 * 1024;
25
26#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
27pub struct AssetId(String);
28
29impl AssetId {
30 pub fn parse(value: impl Into<String>) -> Result<Self, AssetError> {
37 let value = value.into();
38 let segments = value.split('/').collect::<Vec<_>>();
39 let valid = segments.len() >= 2
40 && segments.iter().all(|segment| {
41 !segment.is_empty()
42 && *segment != "."
43 && *segment != ".."
44 && segment.chars().all(|character| {
45 character.is_ascii_alphanumeric() || matches!(character, '_' | '-')
46 })
47 });
48 if valid && !value.contains(':') && !value.contains('\\') {
49 Ok(Self(value))
50 } else {
51 Err(AssetError::InvalidId(value))
52 }
53 }
54
55 #[must_use]
56 pub fn as_str(&self) -> &str {
57 &self.0
58 }
59
60 fn split(&self) -> (&str, &str) {
61 self.0.split_once('/').expect("validated asset ID")
62 }
63}
64
65impl CustomType for AssetId {
66 fn build(mut builder: TypeBuilder<Self>) {
67 builder
68 .with_name("AssetId")
69 .with_fn("to_string", |asset: &mut Self| asset.0.clone());
70 }
71}
72
73#[derive(Clone, Debug, Eq, PartialEq)]
74pub struct AssetData {
75 pub mime_type: String,
76 pub bytes: Vec<u8>,
77}
78
79pub trait AssetProvider {
80 fn load(&self, name: &str) -> Result<AssetData, String>;
87}
88
89#[derive(Clone, Debug, Default)]
90pub struct InMemoryAssetProvider {
91 assets: BTreeMap<String, AssetData>,
92}
93
94impl InMemoryAssetProvider {
95 #[must_use]
96 pub fn new(assets: BTreeMap<String, AssetData>) -> Self {
97 Self { assets }
98 }
99}
100
101impl AssetProvider for InMemoryAssetProvider {
102 fn load(&self, name: &str) -> Result<AssetData, String> {
103 self.assets
104 .get(name)
105 .cloned()
106 .ok_or_else(|| format!("asset `{name}` does not exist"))
107 }
108}
109
110#[derive(Clone, Debug)]
111pub struct DirectoryAssetProvider {
112 root: PathBuf,
113}
114
115impl DirectoryAssetProvider {
116 pub fn new(root: impl AsRef<Path>) -> Result<Self, AssetError> {
122 let path = root.as_ref();
123 Ok(Self {
124 root: path.canonicalize().map_err(|source| AssetError::Io {
125 path: path.to_path_buf(),
126 source,
127 })?,
128 })
129 }
130}
131
132impl AssetProvider for DirectoryAssetProvider {
133 fn load(&self, name: &str) -> Result<AssetData, String> {
134 let direct = self.root.join(name);
135 let path = if direct.exists() {
136 direct
137 } else {
138 [
139 "svg", "png", "jpg", "jpeg", "webp", "gif", "bmp", "tif", "tiff",
140 ]
141 .into_iter()
142 .map(|extension| self.root.join(name).with_extension(extension))
143 .find(|candidate| candidate.exists())
144 .ok_or_else(|| format!("asset `{name}` does not exist"))?
145 };
146 let canonical = path.canonicalize().map_err(|error| error.to_string())?;
147 if !canonical.starts_with(&self.root) {
148 return Err("asset escaped provider root".to_owned());
149 }
150 let mime_type = mime_for_path(&canonical)
151 .ok_or_else(|| "asset has an unsupported image extension".to_owned())?;
152 let bytes = fs::read(&canonical).map_err(|error| error.to_string())?;
153 Ok(AssetData {
154 mime_type: mime_type.to_owned(),
155 bytes,
156 })
157 }
158}
159
160fn mime_for_path(path: &Path) -> Option<&'static str> {
161 match path.extension()?.to_str()?.to_ascii_lowercase().as_str() {
162 "png" => Some("image/png"),
163 "jpg" | "jpeg" => Some("image/jpeg"),
164 "webp" => Some("image/webp"),
165 "gif" => Some("image/gif"),
166 "svg" => Some("image/svg+xml"),
167 "bmp" => Some("image/bmp"),
168 "tif" | "tiff" => Some("image/tiff"),
169 _ => None,
170 }
171}
172
173#[derive(Clone, Debug, Eq, PartialEq)]
174pub struct ImageHandle {
175 opaque: OpaqueHandle,
176 asset: AssetId,
177}
178
179impl ImageHandle {
180 #[must_use]
181 pub fn opaque(&self) -> &OpaqueHandle {
182 &self.opaque
183 }
184
185 #[must_use]
186 pub fn asset(&self) -> &AssetId {
187 &self.asset
188 }
189}
190
191#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
192pub struct ImageDecodeHandle(u64);
193
194impl CustomType for ImageDecodeHandle {
195 fn build(mut builder: TypeBuilder<Self>) {
196 builder
197 .with_name("ImageDecodeHandle")
198 .with_fn("to_string", |handle: &mut Self| {
199 format!("image-decode#{}", handle.0)
200 });
201 }
202}
203
204#[derive(Clone, Debug)]
205struct PendingImageDecode {
206 asset: AssetId,
207 scope: AsyncScope,
208 generation: ScriptGeneration,
209 success: ScriptCallback,
210 error: ScriptCallback,
211 canceled: Arc<AtomicBool>,
212}
213
214struct ImageDecodeMessage {
215 id: u64,
216 result: Result<PreparedImageData, String>,
217}
218
219struct PreparedImageData {
220 data: StoredAssetData,
221 image: Arc<Image>,
222}
223
224#[derive(Clone, Debug)]
225struct StoredAssetData {
226 mime_type: String,
227 content: StoredAssetContent,
228}
229
230#[derive(Clone, Debug)]
231enum StoredAssetContent {
232 Svg(Arc<str>),
233 Raster,
234}
235
236impl StoredAssetData {
237 fn from_validated(data: AssetData, format: ImageFormat) -> Result<Self, AssetError> {
238 let content = if format == ImageFormat::Svg {
239 StoredAssetContent::Svg(Arc::from(
240 String::from_utf8(data.bytes).map_err(|_| AssetError::InvalidSvg)?,
241 ))
242 } else {
243 StoredAssetContent::Raster
244 };
245 Ok(Self {
246 mime_type: data.mime_type,
247 content,
248 })
249 }
250
251 fn svg_source(&self) -> Option<Arc<str>> {
252 match &self.content {
253 StoredAssetContent::Svg(source) => Some(Arc::clone(source)),
254 StoredAssetContent::Raster => None,
255 }
256 }
257}
258
259#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
260struct SvgRasterSource {
261 source: Arc<str>,
262 color: Option<u32>,
263}
264
265impl SvgRasterSource {
266 fn new(source: Arc<str>, color: Option<Rgba8>) -> Self {
267 Self {
268 source,
269 color: color.map(Rgba8::as_rgba_hex),
270 }
271 }
272
273 fn source_bytes(&self) -> usize {
274 self.source.len()
275 }
276
277 fn color(&self) -> Option<Rgba8> {
278 self.color.map(Rgba8::from_rgba_hex)
279 }
280}
281
282#[derive(Clone, Copy, Debug, Eq, PartialEq)]
283pub struct SvgCacheStats {
284 pub entries: usize,
285 pub bytes: usize,
286 pub max_entries: usize,
287 pub max_bytes: usize,
288 pub hits: u64,
289 pub misses: u64,
290 pub evictions: u64,
291}
292
293#[derive(Clone, Debug)]
294struct SvgVariantCacheEntry {
295 bytes: usize,
296 last_access: u64,
297 state: SvgVariantCacheState,
298}
299
300#[derive(Clone, Debug)]
301enum SvgVariantCacheState {
302 Pending {
303 token: u64,
304 canceled: Arc<AtomicBool>,
305 },
306 Ready(Result<Arc<RenderImage>, ImageCacheError>),
307}
308
309struct SvgVariantMessage {
310 source: SvgRasterSource,
311 token: u64,
312 result: Result<Arc<RenderImage>, ImageCacheError>,
313}
314
315struct SvgVariantCache {
316 entries: BTreeMap<SvgRasterSource, SvgVariantCacheEntry>,
317 bytes: usize,
318 max_entries: usize,
319 max_bytes: usize,
320 access_clock: u64,
321 next_token: u64,
322 hits: u64,
323 misses: u64,
324 evictions: u64,
325 sender: Sender<SvgVariantMessage>,
326 receiver: Receiver<SvgVariantMessage>,
327}
328
329impl Default for SvgVariantCache {
330 fn default() -> Self {
331 let (sender, receiver) = channel();
332 Self {
333 entries: BTreeMap::new(),
334 bytes: 0,
335 max_entries: SVG_VARIANT_CACHE_MAX_ENTRIES,
336 max_bytes: SVG_VARIANT_CACHE_MAX_BYTES,
337 access_clock: 0,
338 next_token: 1,
339 hits: 0,
340 misses: 0,
341 evictions: 0,
342 sender,
343 receiver,
344 }
345 }
346}
347
348impl SvgVariantCache {
349 #[cfg(test)]
350 fn with_limits(max_entries: usize, max_bytes: usize) -> Self {
351 let mut cache = Self::default();
352 cache.max_entries = max_entries;
353 cache.max_bytes = max_bytes;
354 cache
355 }
356
357 fn drain(&mut self) {
358 while let Ok(message) = self.receiver.try_recv() {
359 let Some(entry) = self.entries.get_mut(&message.source) else {
360 continue;
361 };
362 let SvgVariantCacheState::Pending { token, canceled } = &entry.state else {
363 continue;
364 };
365 if *token != message.token || canceled.load(Ordering::Acquire) {
366 continue;
367 }
368 let rendered_bytes = message
369 .result
370 .as_ref()
371 .ok()
372 .and_then(|image| image.as_bytes(0))
373 .map_or(0, <[u8]>::len);
374 let new_bytes = message.source.source_bytes().saturating_add(rendered_bytes);
375 self.bytes = self.bytes.saturating_sub(entry.bytes);
376 entry.bytes = new_bytes;
377 entry.state = SvgVariantCacheState::Ready(message.result);
378 self.bytes = self.bytes.saturating_add(new_bytes);
379 }
380 }
381
382 fn clear(&mut self) {
383 for entry in self.entries.values() {
384 if let SvgVariantCacheState::Pending { canceled, .. } = &entry.state {
385 canceled.store(true, Ordering::Release);
386 }
387 }
388 self.evictions = self
389 .evictions
390 .saturating_add(u64::try_from(self.entries.len()).unwrap_or(u64::MAX));
391 self.entries.clear();
392 self.bytes = 0;
393 while self.receiver.try_recv().is_ok() {}
394 }
395
396 fn access(&mut self, source: &SvgRasterSource) -> SvgVariantAccess {
397 self.drain();
398 self.access_clock = self.access_clock.saturating_add(1);
399 if let Some(entry) = self.entries.get_mut(source) {
400 entry.last_access = self.access_clock;
401 self.hits = self.hits.saturating_add(1);
402 let access = match &entry.state {
403 SvgVariantCacheState::Pending { .. } => SvgVariantAccess::Pending,
404 SvgVariantCacheState::Ready(result) => SvgVariantAccess::Ready(result.clone()),
405 };
406 self.enforce_limits(Some(source));
407 return access;
408 }
409 let bytes = source.source_bytes();
410 let token = self.next_token;
411 self.next_token = self.next_token.saturating_add(1);
412 let canceled = Arc::new(AtomicBool::new(false));
413 self.entries.insert(
414 source.clone(),
415 SvgVariantCacheEntry {
416 bytes,
417 last_access: self.access_clock,
418 state: SvgVariantCacheState::Pending {
419 token,
420 canceled: Arc::clone(&canceled),
421 },
422 },
423 );
424 self.bytes = self.bytes.saturating_add(bytes);
425 self.misses = self.misses.saturating_add(1);
426 self.enforce_limits(Some(source));
427 SvgVariantAccess::Start {
428 token,
429 canceled,
430 sender: self.sender.clone(),
431 }
432 }
433
434 fn enforce_limits(&mut self, protected: Option<&SvgRasterSource>) {
435 while self.entries.len() > self.max_entries || self.bytes > self.max_bytes {
436 let candidate = self
437 .entries
438 .iter()
439 .filter(|(source, _)| protected != Some(*source))
440 .min_by_key(|(_, entry)| entry.last_access)
441 .map(|(source, _)| source.clone())
442 .or_else(|| protected.cloned());
443 let Some(candidate) = candidate else {
444 break;
445 };
446 let Some(entry) = self.entries.remove(&candidate) else {
447 break;
448 };
449 if let SvgVariantCacheState::Pending { canceled, .. } = entry.state {
450 canceled.store(true, Ordering::Release);
451 }
452 self.bytes = self.bytes.saturating_sub(entry.bytes);
453 self.evictions = self.evictions.saturating_add(1);
454 }
455 }
456
457 fn stats(&mut self) -> SvgCacheStats {
458 self.drain();
459 self.enforce_limits(None);
460 SvgCacheStats {
461 entries: self.entries.len(),
462 bytes: self.bytes,
463 max_entries: self.max_entries,
464 max_bytes: self.max_bytes,
465 hits: self.hits,
466 misses: self.misses,
467 evictions: self.evictions,
468 }
469 }
470}
471
472impl Drop for SvgVariantCache {
473 fn drop(&mut self) {
474 self.clear();
475 }
476}
477
478enum SvgVariantAccess {
479 Start {
480 token: u64,
481 canceled: Arc<AtomicBool>,
482 sender: Sender<SvgVariantMessage>,
483 },
484 Pending,
485 Ready(Result<Arc<RenderImage>, ImageCacheError>),
486}
487
488#[derive(Clone, Debug)]
489pub(crate) struct ImageDecodeSnapshot {
490 pending: BTreeMap<u64, PendingImageDecode>,
491 deferred: BTreeMap<u64, PendingImageDecode>,
492 transaction_depth: usize,
493}
494
495fn defer_or_cancel_decode(inner: &mut AssetRegistryInner, id: u64, pending: PendingImageDecode) {
496 if inner.transaction_depth > 0 {
497 inner.deferred_decode_cancellations.insert(id, pending);
498 } else {
499 pending.canceled.store(true, Ordering::Release);
500 }
501}
502
503fn cancel_decode_ids(inner: &mut AssetRegistryInner, ids: impl IntoIterator<Item = u64>) {
504 for id in ids {
505 if let Some(pending) = inner.pending_decodes.remove(&id) {
506 defer_or_cancel_decode(inner, id, pending);
507 }
508 }
509}
510
511struct AssetRegistryInner {
512 providers: BTreeMap<String, Box<dyn AssetProvider>>,
513 by_asset: BTreeMap<AssetId, ImageHandle>,
514 images: BTreeMap<u64, Arc<Image>>,
515 image_data: BTreeMap<u64, StoredAssetData>,
516 svg_variants: SvgVariantCache,
517 next_id: u64,
518 next_decode_id: u64,
519 pending_decodes: BTreeMap<u64, PendingImageDecode>,
520 deferred_decode_cancellations: BTreeMap<u64, PendingImageDecode>,
521 transaction_depth: usize,
522 decode_sender: Sender<ImageDecodeMessage>,
523 decode_receiver: Receiver<ImageDecodeMessage>,
524}
525
526impl Default for AssetRegistryInner {
527 fn default() -> Self {
528 let (decode_sender, decode_receiver) = channel();
529 Self {
530 providers: BTreeMap::new(),
531 by_asset: BTreeMap::new(),
532 images: BTreeMap::new(),
533 image_data: BTreeMap::new(),
534 svg_variants: SvgVariantCache::default(),
535 next_id: 0,
536 next_decode_id: 1,
537 pending_decodes: BTreeMap::new(),
538 deferred_decode_cancellations: BTreeMap::new(),
539 transaction_depth: 0,
540 decode_sender,
541 decode_receiver,
542 }
543 }
544}
545
546#[derive(Clone, Default)]
547pub struct AssetRegistry {
548 inner: Rc<RefCell<AssetRegistryInner>>,
549}
550
551impl std::fmt::Debug for AssetRegistry {
552 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
553 match self.inner.try_borrow() {
554 Ok(inner) => formatter
555 .debug_struct("AssetRegistry")
556 .field("providers", &inner.providers.keys().collect::<Vec<_>>())
557 .field("cached", &inner.by_asset.keys().collect::<Vec<_>>())
558 .finish_non_exhaustive(),
559 Err(_) => formatter.write_str("AssetRegistry(<borrowed>)"),
560 }
561 }
562}
563
564impl AssetRegistry {
565 pub(crate) fn begin_transaction(&self) -> Result<ImageDecodeSnapshot, AssetError> {
566 let snapshot = self.decode_snapshot()?;
567 let mut inner = self
568 .inner
569 .try_borrow_mut()
570 .map_err(|_| AssetError::Borrowed)?;
571 inner.transaction_depth = inner.transaction_depth.saturating_add(1);
572 Ok(snapshot)
573 }
574
575 pub(crate) fn commit_transaction(&self) -> Result<(), AssetError> {
576 let mut inner = self
577 .inner
578 .try_borrow_mut()
579 .map_err(|_| AssetError::Borrowed)?;
580 inner.transaction_depth = inner.transaction_depth.saturating_sub(1);
581 if inner.transaction_depth == 0 {
582 for pending in std::mem::take(&mut inner.deferred_decode_cancellations).into_values() {
583 pending.canceled.store(true, Ordering::Release);
584 }
585 }
586 Ok(())
587 }
588
589 #[must_use]
590 pub fn new() -> Self {
591 Self::default()
592 }
593
594 pub fn register(
600 &self,
601 namespace: impl Into<String>,
602 provider: impl AssetProvider + 'static,
603 ) -> Result<(), AssetError> {
604 let namespace = namespace.into();
605 if !valid_segment(&namespace) {
606 return Err(AssetError::InvalidNamespace(namespace));
607 }
608 let mut inner = self
609 .inner
610 .try_borrow_mut()
611 .map_err(|_| AssetError::Borrowed)?;
612 if inner.providers.contains_key(&namespace) {
613 return Err(AssetError::DuplicateNamespace(namespace));
614 }
615 inner.providers.insert(namespace, Box::new(provider));
616 Ok(())
617 }
618
619 pub fn refresh_namespace(&self, namespace: &str) -> Result<usize, AssetError> {
627 let refreshed = {
628 let inner = self.inner.try_borrow().map_err(|_| AssetError::Borrowed)?;
629 let provider = inner
630 .providers
631 .get(namespace)
632 .ok_or_else(|| AssetError::UnknownNamespace(namespace.to_owned()))?;
633 inner
634 .by_asset
635 .iter()
636 .filter(|(asset, _)| asset.split().0 == namespace)
637 .map(|(asset, handle)| {
638 let (_, name) = asset.split();
639 let data = provider
640 .load(name)
641 .map_err(|message| AssetError::Provider {
642 asset: asset.clone(),
643 message,
644 })?;
645 let format = ImageFormat::from_mime_type(&data.mime_type)
646 .ok_or_else(|| AssetError::UnsupportedMime(data.mime_type.clone()))?;
647 if data.bytes.is_empty() {
648 return Err(AssetError::Empty(asset.clone()));
649 }
650 let image = prepare_image(format, &data.bytes, None)?;
651 let stored = StoredAssetData::from_validated(data, format)?;
652 Ok((handle.opaque.id(), stored, image))
653 })
654 .collect::<Result<Vec<_>, AssetError>>()?
655 };
656 let mut inner = self
657 .inner
658 .try_borrow_mut()
659 .map_err(|_| AssetError::Borrowed)?;
660 for (id, data, image) in &refreshed {
661 inner.images.insert(*id, Arc::clone(image));
662 inner.image_data.insert(*id, data.clone());
663 }
664 if !refreshed.is_empty() {
665 inner.svg_variants.clear();
666 }
667 Ok(refreshed.len())
668 }
669
670 pub fn load_image(&self, id: &AssetId) -> Result<ImageHandle, AssetError> {
676 if let Some(handle) = self
677 .inner
678 .try_borrow()
679 .map_err(|_| AssetError::Borrowed)?
680 .by_asset
681 .get(id)
682 .cloned()
683 {
684 return Ok(handle);
685 }
686 let (namespace, name) = id.split();
687 let data = {
688 let inner = self.inner.try_borrow().map_err(|_| AssetError::Borrowed)?;
689 inner
690 .providers
691 .get(namespace)
692 .ok_or_else(|| AssetError::UnknownNamespace(namespace.to_owned()))?
693 .load(name)
694 .map_err(|message| AssetError::Provider {
695 asset: id.clone(),
696 message,
697 })?
698 };
699 let format = ImageFormat::from_mime_type(&data.mime_type)
700 .ok_or_else(|| AssetError::UnsupportedMime(data.mime_type.clone()))?;
701 if data.bytes.is_empty() {
702 return Err(AssetError::Empty(id.clone()));
703 }
704 let image = prepare_image(format, &data.bytes, None)?;
705 let data = StoredAssetData::from_validated(data, format)?;
706 let mut inner = self
707 .inner
708 .try_borrow_mut()
709 .map_err(|_| AssetError::Borrowed)?;
710 inner.next_id = inner.next_id.saturating_add(1);
711 let image_id = inner.next_id;
712 let handle = ImageHandle {
713 opaque: OpaqueHandle::new("image", image_id),
714 asset: id.clone(),
715 };
716 inner.images.insert(image_id, image);
717 inner.image_data.insert(image_id, data);
718 inner.by_asset.insert(id.clone(), handle.clone());
719 Ok(handle)
720 }
721
722 pub fn preload_images(
729 &self,
730 ids: impl IntoIterator<Item = AssetId>,
731 ) -> Result<usize, AssetError> {
732 let mut loaded = 0;
733 for id in ids {
734 self.load_image(&id)?;
735 loaded += 1;
736 }
737 Ok(loaded)
738 }
739
740 pub fn cached_image(&self, id: &AssetId) -> Result<ImageHandle, AssetError> {
746 self.inner
747 .try_borrow()
748 .map_err(|_| AssetError::Borrowed)?
749 .by_asset
750 .get(id)
751 .cloned()
752 .ok_or_else(|| AssetError::NotPreloaded(id.clone()))
753 }
754
755 pub fn start_image_decode(
762 &self,
763 id: &AssetId,
764 scope: AsyncScope,
765 generation: ScriptGeneration,
766 success: ScriptCallback,
767 error: ScriptCallback,
768 ) -> Result<ImageDecodeHandle, AssetError> {
769 let (namespace, name) = id.split();
770 let data = {
771 let inner = self.inner.try_borrow().map_err(|_| AssetError::Borrowed)?;
772 inner
773 .providers
774 .get(namespace)
775 .ok_or_else(|| AssetError::UnknownNamespace(namespace.to_owned()))?
776 .load(name)
777 .map_err(|message| AssetError::Provider {
778 asset: id.clone(),
779 message,
780 })?
781 };
782 let (decode_id, sender, canceled) = {
783 let mut inner = self
784 .inner
785 .try_borrow_mut()
786 .map_err(|_| AssetError::Borrowed)?;
787 let decode_id = inner.next_decode_id;
788 inner.next_decode_id = inner.next_decode_id.saturating_add(1);
789 let canceled = Arc::new(AtomicBool::new(false));
790 inner.pending_decodes.insert(
791 decode_id,
792 PendingImageDecode {
793 asset: id.clone(),
794 scope,
795 generation,
796 success,
797 error,
798 canceled: canceled.clone(),
799 },
800 );
801 (decode_id, inner.decode_sender.clone(), canceled)
802 };
803 let worker_cancel = canceled.clone();
804 if let Err(source) = std::thread::Builder::new()
805 .name("gpui-rhai-image-decode".to_owned())
806 .spawn(move || {
807 if worker_cancel.load(Ordering::Acquire) {
808 return;
809 }
810 let result = decode_asset_data(data);
811 if !worker_cancel.load(Ordering::Acquire) {
812 let _ = sender.send(ImageDecodeMessage {
813 id: decode_id,
814 result,
815 });
816 }
817 })
818 {
819 canceled.store(true, Ordering::Release);
820 self.inner
821 .try_borrow_mut()
822 .map_err(|_| AssetError::Borrowed)?
823 .pending_decodes
824 .remove(&decode_id);
825 return Err(AssetError::DecodeSpawn(source));
826 }
827 Ok(ImageDecodeHandle(decode_id))
828 }
829
830 pub fn cancel_image_decode(&self, handle: ImageDecodeHandle) -> Result<bool, AssetError> {
836 let mut inner = self
837 .inner
838 .try_borrow_mut()
839 .map_err(|_| AssetError::Borrowed)?;
840 let Some(pending) = inner.pending_decodes.remove(&handle.0) else {
841 return Ok(false);
842 };
843 defer_or_cancel_decode(&mut inner, handle.0, pending);
844 Ok(true)
845 }
846
847 pub fn cancel_window_scope(
853 &self,
854 window: &str,
855 component: &ComponentInstancePath,
856 ) -> Result<(), AssetError> {
857 let mut inner = self
858 .inner
859 .try_borrow_mut()
860 .map_err(|_| AssetError::Borrowed)?;
861 let remove = inner
862 .pending_decodes
863 .iter()
864 .filter_map(|(id, pending)| {
865 let remove = match &pending.scope {
866 AsyncScope::Window(id) => id == window,
867 AsyncScope::Component(_) | AsyncScope::Effect { .. } => {
868 pending.scope.is_within_component(component)
869 }
870 AsyncScope::App => false,
871 };
872 remove.then_some(*id)
873 })
874 .collect::<Vec<_>>();
875 cancel_decode_ids(&mut inner, remove);
876 Ok(())
877 }
878
879 pub fn cancel_component_scope(
885 &self,
886 component: &ComponentInstancePath,
887 ) -> Result<(), AssetError> {
888 let mut inner = self
889 .inner
890 .try_borrow_mut()
891 .map_err(|_| AssetError::Borrowed)?;
892 let remove = inner
893 .pending_decodes
894 .iter()
895 .filter_map(|(id, pending)| pending.scope.is_within_component(component).then_some(*id))
896 .collect::<Vec<_>>();
897 cancel_decode_ids(&mut inner, remove);
898 Ok(())
899 }
900
901 pub fn cancel_scope(&self, scope: &AsyncScope) -> Result<(), AssetError> {
907 let mut inner = self
908 .inner
909 .try_borrow_mut()
910 .map_err(|_| AssetError::Borrowed)?;
911 let remove = inner
912 .pending_decodes
913 .iter()
914 .filter_map(|(id, pending)| (&pending.scope == scope).then_some(*id))
915 .collect::<Vec<_>>();
916 cancel_decode_ids(&mut inner, remove);
917 Ok(())
918 }
919
920 pub fn retain_decode_generation(&self, generation: ScriptGeneration) -> Result<(), AssetError> {
926 let mut inner = self
927 .inner
928 .try_borrow_mut()
929 .map_err(|_| AssetError::Borrowed)?;
930 inner.pending_decodes.retain(|_, pending| {
931 let retain = pending.generation == generation;
932 if !retain {
933 pending.canceled.store(true, Ordering::Release);
934 }
935 retain
936 });
937 Ok(())
938 }
939
940 pub fn drain_image_decodes(
946 &self,
947 generation: ScriptGeneration,
948 ) -> Result<Vec<AsyncDelivery>, AssetError> {
949 let mut inner = self
950 .inner
951 .try_borrow_mut()
952 .map_err(|_| AssetError::Borrowed)?;
953 let mut messages = Vec::new();
954 while let Ok(message) = inner.decode_receiver.try_recv() {
955 messages.push(message);
956 }
957 let mut deliveries = Vec::new();
958 for message in messages {
959 let Some(pending) = inner.pending_decodes.remove(&message.id) else {
960 continue;
961 };
962 if pending.canceled.load(Ordering::Acquire) || pending.generation != generation {
963 continue;
964 }
965 let (callback, payload) = match message.result {
966 Ok(prepared) => {
967 let handle = install_decoded_image(&mut inner, pending.asset, prepared);
968 (pending.success, UiValue::Handle(handle.opaque().clone()))
969 }
970 Err(message) => (pending.error, UiValue::String(message)),
971 };
972 deliveries.push(AsyncDelivery {
973 callback,
974 payload,
975 scope: pending.scope,
976 });
977 }
978 Ok(deliveries)
979 }
980
981 #[must_use]
982 pub fn pending_decode_count(&self) -> usize {
983 self.inner
984 .try_borrow()
985 .map_or(0, |inner| inner.pending_decodes.len())
986 }
987
988 pub(crate) fn decode_snapshot(&self) -> Result<ImageDecodeSnapshot, AssetError> {
989 let inner = self.inner.try_borrow().map_err(|_| AssetError::Borrowed)?;
990 Ok(ImageDecodeSnapshot {
991 pending: inner.pending_decodes.clone(),
992 deferred: inner.deferred_decode_cancellations.clone(),
993 transaction_depth: inner.transaction_depth,
994 })
995 }
996
997 pub(crate) fn restore_decode_snapshot(
998 &self,
999 snapshot: ImageDecodeSnapshot,
1000 ) -> Result<(), AssetError> {
1001 let mut inner = self
1002 .inner
1003 .try_borrow_mut()
1004 .map_err(|_| AssetError::Borrowed)?;
1005 for (id, pending) in inner
1006 .pending_decodes
1007 .iter()
1008 .chain(inner.deferred_decode_cancellations.iter())
1009 {
1010 if !snapshot.pending.contains_key(id) && !snapshot.deferred.contains_key(id) {
1011 pending.canceled.store(true, Ordering::Release);
1012 }
1013 }
1014 inner.pending_decodes = snapshot.pending;
1015 inner.deferred_decode_cancellations = snapshot.deferred;
1016 inner.transaction_depth = snapshot.transaction_depth;
1017 Ok(())
1018 }
1019
1020 pub fn image_source(&self, handle: &OpaqueHandle) -> Result<ImageSource, AssetError> {
1026 self.image_source_tinted(handle, None)
1027 }
1028
1029 pub fn image_source_tinted(
1036 &self,
1037 handle: &OpaqueHandle,
1038 color: Option<Rgba8>,
1039 ) -> Result<ImageSource, AssetError> {
1040 if handle.kind() != "image" {
1041 return Err(AssetError::WrongHandleKind(handle.kind().to_owned()));
1042 }
1043 let inner = self.inner.try_borrow().map_err(|_| AssetError::Borrowed)?;
1044 let Some(data) = inner.image_data.get(&handle.id()) else {
1045 return Err(AssetError::UnknownHandle(handle.id()));
1046 };
1047 if data.mime_type == "image/svg+xml"
1048 && let Some(color) = color
1049 && let Some(source) = data.svg_source()
1050 && svg_uses_external_current_color(&source)
1051 {
1052 drop(inner);
1053 return Ok(self.svg_variant_image_source(SvgRasterSource::new(source, Some(color))));
1054 }
1055 let image = inner
1056 .images
1057 .get(&handle.id())
1058 .cloned()
1059 .ok_or(AssetError::UnknownHandle(handle.id()))?;
1060 Ok(ImageSource::Image(image))
1061 }
1062
1063 pub(crate) fn inline_svg_source(&self, source: Arc<str>, color: Option<Rgba8>) -> ImageSource {
1064 self.svg_variant_image_source(SvgRasterSource::new(source, color))
1065 }
1066
1067 fn svg_variant_image_source(&self, source: SvgRasterSource) -> ImageSource {
1068 let registry = self.clone();
1069 ImageSource::from(move |window: &mut gpui::Window, cx: &mut gpui::App| {
1070 let access = {
1071 let Ok(mut inner) = registry.inner.try_borrow_mut() else {
1072 return Some(Err(ImageCacheError::Asset(
1073 AssetError::Borrowed.to_string().into(),
1074 )));
1075 };
1076 inner.svg_variants.access(&source)
1077 };
1078 match access {
1079 SvgVariantAccess::Ready(result) => Some(result),
1080 SvgVariantAccess::Pending => {
1081 window.request_animation_frame();
1082 None
1083 }
1084 SvgVariantAccess::Start {
1085 token,
1086 canceled,
1087 sender,
1088 } => {
1089 let source = source.clone();
1090 cx.background_spawn(async move {
1091 if canceled.load(Ordering::Acquire) {
1092 return;
1093 }
1094 let result = svg_render_image(source.source.as_bytes(), source.color())
1095 .map_err(|error| ImageCacheError::Asset(error.to_string().into()));
1096 if !canceled.load(Ordering::Acquire) {
1097 let _ = sender.send(SvgVariantMessage {
1098 source,
1099 token,
1100 result,
1101 });
1102 }
1103 })
1104 .detach();
1105 window.request_animation_frame();
1106 None
1107 }
1108 }
1109 })
1110 }
1111
1112 pub fn svg_cache_stats(&self) -> Result<SvgCacheStats, AssetError> {
1121 self.inner
1122 .try_borrow_mut()
1123 .map(|mut inner| inner.svg_variants.stats())
1124 .map_err(|_| AssetError::Borrowed)
1125 }
1126}
1127
1128fn install_decoded_image(
1129 inner: &mut AssetRegistryInner,
1130 asset: AssetId,
1131 prepared: PreparedImageData,
1132) -> ImageHandle {
1133 if let Some(handle) = inner.by_asset.get(&asset) {
1134 return handle.clone();
1135 }
1136 inner.next_id = inner.next_id.saturating_add(1);
1137 let image_id = inner.next_id;
1138 let handle = ImageHandle {
1139 opaque: OpaqueHandle::new("image", image_id),
1140 asset: asset.clone(),
1141 };
1142 inner.images.insert(image_id, prepared.image);
1143 inner.image_data.insert(image_id, prepared.data);
1144 inner.by_asset.insert(asset, handle.clone());
1145 handle
1146}
1147
1148fn prepare_image(
1149 format: ImageFormat,
1150 bytes: &[u8],
1151 color: Option<Rgba8>,
1152) -> Result<Arc<Image>, AssetError> {
1153 if format == ImageFormat::Svg {
1154 svg_image(bytes, color)
1155 } else {
1156 Ok(Arc::new(Image::from_bytes(format, bytes.to_vec())))
1157 }
1158}
1159
1160fn decode_asset_data(data: AssetData) -> Result<PreparedImageData, String> {
1161 if data.bytes.is_empty() {
1162 return Err("image data is empty".to_owned());
1163 }
1164 let format = ImageFormat::from_mime_type(&data.mime_type)
1165 .ok_or_else(|| format!("unsupported image MIME type `{}`", data.mime_type))?;
1166 if format == ImageFormat::Svg {
1167 let source = std::str::from_utf8(&data.bytes)
1168 .map_err(|_| "SVG image is not valid UTF-8".to_owned())?;
1169 if !source.contains("<svg") {
1170 return Err("SVG image has no root element".to_owned());
1171 }
1172 } else {
1173 let raster_format = raster_image_format(format)
1174 .ok_or_else(|| "unsupported raster image format".to_owned())?;
1175 image::load_from_memory_with_format(&data.bytes, raster_format)
1176 .map_err(|error| format!("raster image decode failed: {error}"))?;
1177 }
1178 let image = prepare_image(format, &data.bytes, None).map_err(|error| error.to_string())?;
1179 let data = StoredAssetData::from_validated(data, format).map_err(|error| error.to_string())?;
1180 Ok(PreparedImageData { data, image })
1181}
1182
1183fn raster_image_format(format: ImageFormat) -> Option<image::ImageFormat> {
1184 match format {
1185 ImageFormat::Png => Some(image::ImageFormat::Png),
1186 ImageFormat::Jpeg => Some(image::ImageFormat::Jpeg),
1187 ImageFormat::Webp => Some(image::ImageFormat::WebP),
1188 ImageFormat::Gif => Some(image::ImageFormat::Gif),
1189 ImageFormat::Bmp => Some(image::ImageFormat::Bmp),
1190 ImageFormat::Tiff => Some(image::ImageFormat::Tiff),
1191 ImageFormat::Svg | ImageFormat::Ico | ImageFormat::Pnm => None,
1192 }
1193}
1194
1195pub(crate) fn svg_image(bytes: &[u8], color: Option<Rgba8>) -> Result<Arc<Image>, AssetError> {
1196 let pixmap = svg_pixmap(bytes, color)?;
1203 let png = pixmap.encode_png().map_err(|_| AssetError::InvalidSvg)?;
1204 Ok(Arc::new(Image::from_bytes(ImageFormat::Png, png)))
1205}
1206
1207fn svg_render_image(bytes: &[u8], color: Option<Rgba8>) -> Result<Arc<RenderImage>, AssetError> {
1208 let pixmap = svg_pixmap(bytes, color)?;
1209 let png = pixmap.encode_png().map_err(|_| AssetError::InvalidSvg)?;
1210 let mut bgra = image::load_from_memory_with_format(&png, image::ImageFormat::Png)
1211 .map_err(|_| AssetError::InvalidSvg)?
1212 .into_rgba8();
1213 for pixel in bgra.chunks_exact_mut(4) {
1214 pixel.swap(0, 2);
1215 }
1216 Ok(Arc::new(RenderImage::new(vec![image::Frame::new(bgra)])))
1217}
1218
1219fn svg_pixmap(bytes: &[u8], color: Option<Rgba8>) -> Result<resvg::tiny_skia::Pixmap, AssetError> {
1220 let source = std::str::from_utf8(bytes).map_err(|_| AssetError::InvalidSvg)?;
1221 let source = inherited_svg_color(source, color)?;
1222 let tree = usvg::Tree::from_str(&source, &svg_options()).map_err(|_| AssetError::InvalidSvg)?;
1223 let width = svg_raster_dimension(tree.size().width()).ok_or(AssetError::InvalidSvg)?;
1224 let height = svg_raster_dimension(tree.size().height()).ok_or(AssetError::InvalidSvg)?;
1225 if u64::from(width).saturating_mul(u64::from(height)) > MAX_SVG_RASTER_PIXELS {
1226 return Err(AssetError::InvalidSvg);
1227 }
1228 let mut pixmap = resvg::tiny_skia::Pixmap::new(width, height).ok_or(AssetError::InvalidSvg)?;
1229 resvg::render(
1230 &tree,
1231 resvg::tiny_skia::Transform::identity(),
1232 &mut pixmap.as_mut(),
1233 );
1234 Ok(pixmap)
1235}
1236
1237pub(crate) fn svg_options() -> usvg::Options<'static> {
1238 static FONT_DB: LazyLock<Arc<usvg::fontdb::Database>> = LazyLock::new(|| {
1239 let mut database = usvg::fontdb::Database::new();
1240 database.load_system_fonts();
1241 let first_family = database
1242 .faces()
1243 .find_map(|face| face.families.first().map(|family| family.0.clone()));
1244 if let Some(family) = available_font_family(
1245 &database,
1246 &[
1247 "Arial",
1248 "Helvetica",
1249 "DejaVu Sans",
1250 "Liberation Sans",
1251 "Noto Sans",
1252 ],
1253 )
1254 .or_else(|| first_family.clone())
1255 {
1256 database.set_sans_serif_family(family.clone());
1257 database.set_cursive_family(family.clone());
1258 database.set_fantasy_family(family);
1259 }
1260 if let Some(family) = available_font_family(
1261 &database,
1262 &[
1263 "Times New Roman",
1264 "Times",
1265 "DejaVu Serif",
1266 "Liberation Serif",
1267 "Noto Serif",
1268 ],
1269 )
1270 .or_else(|| first_family.clone())
1271 {
1272 database.set_serif_family(family);
1273 }
1274 if let Some(family) = available_font_family(
1275 &database,
1276 &[
1277 "Courier New",
1278 "Menlo",
1279 "DejaVu Sans Mono",
1280 "Liberation Mono",
1281 "Noto Sans Mono",
1282 ],
1283 )
1284 .or(first_family)
1285 {
1286 database.set_monospace_family(family);
1287 }
1288 Arc::new(database)
1289 });
1290 let default_font_resolver = usvg::FontResolver::default_font_selector();
1291 let font_resolver = Box::new(
1292 move |font: &usvg::Font, database: &mut Arc<usvg::fontdb::Database>| {
1293 if database.is_empty() {
1294 *database = Arc::clone(&FONT_DB);
1295 }
1296 default_font_resolver(font, database)
1297 },
1298 );
1299 usvg::Options {
1300 font_resolver: usvg::FontResolver {
1301 select_font: font_resolver,
1302 select_fallback: usvg::FontResolver::default_fallback_selector(),
1303 },
1304 ..Default::default()
1305 }
1306}
1307
1308fn available_font_family(database: &usvg::fontdb::Database, candidates: &[&str]) -> Option<String> {
1309 candidates.iter().find_map(|candidate| {
1310 database.faces().find_map(|face| {
1311 face.families
1312 .iter()
1313 .find(|family| family.0.eq_ignore_ascii_case(candidate))
1314 .map(|family| family.0.clone())
1315 })
1316 })
1317}
1318
1319fn svg_raster_dimension(value: f32) -> Option<u32> {
1320 let value = value.ceil();
1321 if !value.is_finite() || !(1.0..=MAX_SVG_RASTER_DIMENSION_F32).contains(&value) {
1322 return None;
1323 }
1324 let parsed = value.to_string().parse::<u32>().ok()?;
1325 (parsed <= MAX_SVG_RASTER_DIMENSION).then_some(parsed)
1326}
1327
1328fn inherited_svg_color(source: &str, color: Option<Rgba8>) -> Result<String, AssetError> {
1329 let document = roxmltree::Document::parse(source).map_err(|_| AssetError::InvalidSvg)?;
1330 let root = document.root_element();
1331 if root.tag_name().name() != "svg" {
1332 return Err(AssetError::InvalidSvg);
1333 }
1334 let Some(color) = color else {
1335 return Ok(source.to_owned());
1336 };
1337 if let Some(attribute) = root
1338 .attributes()
1339 .find(|attribute| attribute.name() == "color")
1340 {
1341 if is_explicit_svg_color_value(attribute.value()) {
1342 return Ok(source.to_owned());
1343 }
1344 let range = attribute.range();
1345 let mut inherited = String::with_capacity(source.len().saturating_add(8));
1346 inherited.push_str(&source[..range.start]);
1347 let _ = write!(inherited, "color=\"#{:08x}\"", color.as_rgba_hex());
1348 inherited.push_str(&source[range.end..]);
1349 return Ok(inherited);
1350 }
1351 let root_start = root.range().start;
1352 let bytes = source.as_bytes();
1353 let mut name_end = root_start.saturating_add(1);
1354 while bytes
1355 .get(name_end)
1356 .is_some_and(|byte| !byte.is_ascii_whitespace() && !matches!(byte, b'/' | b'>'))
1357 {
1358 name_end = name_end.saturating_add(1);
1359 }
1360 if name_end <= root_start.saturating_add(1) || name_end > source.len() {
1361 return Err(AssetError::InvalidSvg);
1362 }
1363 let mut inherited = String::with_capacity(source.len().saturating_add(20));
1364 inherited.push_str(&source[..name_end]);
1365 let _ = write!(inherited, " color=\"#{:08x}\"", color.as_rgba_hex());
1366 inherited.push_str(&source[name_end..]);
1367 Ok(inherited)
1368}
1369
1370fn svg_uses_external_current_color(source: &str) -> bool {
1371 if !source
1372 .as_bytes()
1373 .windows(b"currentColor".len())
1374 .any(|candidate| candidate.eq_ignore_ascii_case(b"currentColor"))
1375 {
1376 return false;
1377 }
1378 let Ok(document) = roxmltree::Document::parse(source) else {
1379 return true;
1380 };
1381 let mut found_attribute_use = false;
1382 for node in document.descendants().filter(roxmltree::Node::is_element) {
1383 let uses_current_color = node.attributes().any(|attribute| {
1384 attribute
1385 .value()
1386 .as_bytes()
1387 .windows(b"currentColor".len())
1388 .any(|candidate| candidate.eq_ignore_ascii_case(b"currentColor"))
1389 });
1390 if !uses_current_color {
1391 continue;
1392 }
1393 found_attribute_use = true;
1394 let mut ancestor = Some(node);
1395 let mut locally_resolved = false;
1396 while let Some(element) = ancestor {
1397 if element_has_explicit_color(element) {
1398 locally_resolved = true;
1399 break;
1400 }
1401 ancestor = element.parent_element();
1402 }
1403 if !locally_resolved {
1404 return true;
1405 }
1406 }
1407 !found_attribute_use
1408}
1409
1410fn element_has_explicit_color(node: roxmltree::Node<'_, '_>) -> bool {
1411 if node
1412 .attribute("color")
1413 .is_some_and(is_explicit_svg_color_value)
1414 {
1415 return true;
1416 }
1417 node.attribute("style").is_some_and(|style| {
1418 style.split(';').any(|declaration| {
1419 let Some((name, value)) = declaration.split_once(':') else {
1420 return false;
1421 };
1422 name.trim().eq_ignore_ascii_case("color") && is_explicit_svg_color_value(value)
1423 })
1424 })
1425}
1426
1427fn is_explicit_svg_color_value(value: &str) -> bool {
1428 let value = value.trim();
1429 !value.eq_ignore_ascii_case("inherit")
1430 && !value.eq_ignore_ascii_case("currentColor")
1431 && !value.eq_ignore_ascii_case("unset")
1432 && !value.eq_ignore_ascii_case("revert")
1433}
1434
1435fn valid_segment(value: &str) -> bool {
1436 !value.is_empty()
1437 && value
1438 .chars()
1439 .all(|character| character.is_ascii_alphanumeric() || matches!(character, '_' | '-'))
1440}
1441
1442pub(crate) fn asset_id_from_script(value: &str) -> Result<AssetId, Box<rhai::EvalAltResult>> {
1443 AssetId::parse(value).map_err(|error| error.to_string().into())
1444}
1445
1446#[derive(Debug, Error)]
1447pub enum AssetError {
1448 #[error("asset ID `{0}` must be a namespaced logical path")]
1449 InvalidId(String),
1450 #[error("asset namespace `{0}` is invalid")]
1451 InvalidNamespace(String),
1452 #[error("asset namespace `{0}` is already registered")]
1453 DuplicateNamespace(String),
1454 #[error("asset namespace `{0}` is not registered")]
1455 UnknownNamespace(String),
1456 #[error("asset registry is already borrowed")]
1457 Borrowed,
1458 #[error("asset provider failed for `{asset:?}`: {message}")]
1459 Provider { asset: AssetId, message: String },
1460 #[error("image MIME type `{0}` is unsupported")]
1461 UnsupportedMime(String),
1462 #[error("image asset `{0:?}` is empty")]
1463 Empty(AssetId),
1464 #[error("SVG asset is not valid UTF-8 SVG data")]
1465 InvalidSvg,
1466 #[error("failed to spawn image decode worker: {0}")]
1467 DecodeSpawn(std::io::Error),
1468 #[error("opaque handle kind `{0}` is not an image")]
1469 WrongHandleKind(String),
1470 #[error("image handle `{0}` is unknown")]
1471 UnknownHandle(u64),
1472 #[error("declarative image asset `{0:?}` was not preloaded during preparation")]
1473 NotPreloaded(AssetId),
1474 #[error("asset I/O failed for `{path}`: {source}")]
1475 Io {
1476 path: PathBuf,
1477 #[source]
1478 source: std::io::Error,
1479 },
1480}
1481
1482#[cfg(test)]
1483mod tests {
1484 use super::*;
1485 use std::io::Cursor;
1486 use std::time::Duration;
1487
1488 use rhai::FnPtr;
1489
1490 #[derive(Clone)]
1491 struct MutableProvider(Rc<RefCell<AssetData>>);
1492
1493 impl AssetProvider for MutableProvider {
1494 fn load(&self, _: &str) -> Result<AssetData, String> {
1495 Ok(self.0.borrow().clone())
1496 }
1497 }
1498
1499 fn one_pixel_png() -> Vec<u8> {
1500 let mut output = Cursor::new(Vec::new());
1501 image::DynamicImage::new_rgba8(1, 1)
1502 .write_to(&mut output, image::ImageFormat::Png)
1503 .unwrap();
1504 output.into_inner()
1505 }
1506
1507 #[test]
1508 fn logical_ids_reject_urls_and_traversal() {
1509 for invalid in ["https://example.com/a.png", "/tmp/a.png", "core/../secret"] {
1510 assert!(AssetId::parse(invalid).is_err());
1511 }
1512 assert_eq!(AssetId::parse("core/check").unwrap().as_str(), "core/check");
1513 }
1514
1515 #[test]
1516 fn provider_images_are_cached_behind_opaque_handles() {
1517 let registry = AssetRegistry::new();
1518 registry
1519 .register(
1520 "core",
1521 InMemoryAssetProvider::new(BTreeMap::from([(
1522 "check".to_owned(),
1523 AssetData {
1524 mime_type: "image/svg+xml".to_owned(),
1525 bytes: br#"<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"/>"#
1526 .to_vec(),
1527 },
1528 )])),
1529 )
1530 .unwrap();
1531 let id = AssetId::parse("core/check").unwrap();
1532 let first = registry.load_image(&id).unwrap();
1533 let second = registry.load_image(&id).unwrap();
1534 assert_eq!(first, second);
1535 registry.image_source(first.opaque()).unwrap();
1536 assert!(matches!(
1537 registry.image_source(&OpaqueHandle::new("task", 1)),
1538 Err(AssetError::WrongHandleKind(_))
1539 ));
1540 }
1541
1542 #[test]
1543 fn rejected_duplicate_provider_keeps_uncached_assets_from_the_original() {
1544 let registry = AssetRegistry::new();
1545 let data = AssetData {
1546 mime_type: "image/svg+xml".to_owned(),
1547 bytes: br#"<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"/>"#.to_vec(),
1548 };
1549 registry
1550 .register(
1551 "app",
1552 InMemoryAssetProvider::new(BTreeMap::from([
1553 ("first".to_owned(), data.clone()),
1554 ("second".to_owned(), data),
1555 ])),
1556 )
1557 .unwrap();
1558 registry
1559 .load_image(&AssetId::parse("app/first").unwrap())
1560 .unwrap();
1561 assert!(matches!(
1562 registry.register("app", InMemoryAssetProvider::default()),
1563 Err(AssetError::DuplicateNamespace(_))
1564 ));
1565 registry
1566 .load_image(&AssetId::parse("app/second").unwrap())
1567 .unwrap();
1568 }
1569
1570 #[test]
1571 fn namespace_refresh_preserves_handle_and_replaces_cached_bytes() {
1572 let data = Rc::new(RefCell::new(AssetData {
1573 mime_type: "image/svg+xml".to_owned(),
1574 bytes: br#"<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1" data-version="one"/>"#.to_vec(),
1575 }));
1576 let registry = AssetRegistry::new();
1577 registry
1578 .register("app", MutableProvider(Rc::clone(&data)))
1579 .unwrap();
1580 let id = AssetId::parse("app/icon").unwrap();
1581 let handle = registry.load_image(&id).unwrap();
1582 data.borrow_mut().bytes =
1583 br#"<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1" data-version="two"/>"#
1584 .to_vec();
1585 assert_eq!(registry.refresh_namespace("app").unwrap(), 1);
1586 assert_eq!(registry.load_image(&id).unwrap(), handle);
1587 assert_eq!(
1588 registry.inner.borrow().image_data[&handle.opaque.id()]
1589 .svg_source()
1590 .unwrap()
1591 .as_bytes(),
1592 br#"<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1" data-version="two"/>"#
1593 );
1594 }
1595
1596 #[test]
1597 fn svg_current_color_uses_external_default_without_overriding_local_color() {
1598 let inherited = br#"<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"><rect width="1" height="1" fill="currentColor"/></svg>"#;
1599 assert!(svg_uses_external_current_color(
1600 std::str::from_utf8(inherited).unwrap()
1601 ));
1602 let image = svg_image(inherited, Some(Rgba8::from_rgba_hex(0x1234_ab80))).unwrap();
1603 assert_eq!(
1604 image::load_from_memory(&image.bytes)
1605 .unwrap()
1606 .into_rgba8()
1607 .into_raw(),
1608 [0x12, 0x34, 0xab, 0x80]
1609 );
1610
1611 let local = br##"<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"><rect width="1" height="1" color="#ff0000" fill="currentColor"/></svg>"##;
1612 assert!(!svg_uses_external_current_color(
1613 std::str::from_utf8(local).unwrap()
1614 ));
1615 let image = svg_image(local, Some(Rgba8::from_rgba_hex(0x00ff_00ff))).unwrap();
1616 assert_eq!(
1617 image::load_from_memory(&image.bytes)
1618 .unwrap()
1619 .into_rgba8()
1620 .into_raw(),
1621 [0xff, 0x00, 0x00, 0xff]
1622 );
1623
1624 let root_inherit = br#"<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1" color="inherit"><rect width="1" height="1" fill="currentColor"/></svg>"#;
1625 let image = svg_image(root_inherit, Some(Rgba8::from_rgba_hex(0x1234_abff))).unwrap();
1626 assert_eq!(
1627 image::load_from_memory(&image.bytes)
1628 .unwrap()
1629 .into_rgba8()
1630 .into_raw(),
1631 [0x12, 0x34, 0xab, 0xff]
1632 );
1633 }
1634
1635 #[test]
1636 fn svg_raster_adapter_preserves_fixed_color_gradient_and_semantic_alpha() {
1637 let mixed = br##"<svg xmlns="http://www.w3.org/2000/svg" width="2" height="1"><rect width="1" height="1" fill="currentColor"/><rect x="1" width="1" height="1" fill="#ff0000"/></svg>"##;
1638 let image = svg_image(mixed, Some(Rgba8::from_rgba_hex(0x1234_ab80))).unwrap();
1639 let pixels = image::load_from_memory(&image.bytes)
1640 .unwrap()
1641 .into_rgba8()
1642 .into_raw();
1643 assert_eq!(&pixels[0..4], &[0x12, 0x34, 0xab, 0x80]);
1644 assert_eq!(&pixels[4..8], &[0xff, 0x00, 0x00, 0xff]);
1645
1646 let gradient = br##"<svg xmlns="http://www.w3.org/2000/svg" width="4" height="1"><defs><linearGradient id="g"><stop offset="0" stop-color="#ff0000"/><stop offset="1" stop-color="#0000ff"/></linearGradient></defs><rect width="4" height="1" fill="url(#g)"/></svg>"##;
1647 let image = svg_image(gradient, None).unwrap();
1648 let pixels = image::load_from_memory(&image.bytes)
1649 .unwrap()
1650 .into_rgba8()
1651 .into_raw();
1652 assert!(pixels[0] > pixels[2], "gradient must begin red: {pixels:?}");
1653 let last = &pixels[pixels.len() - 4..];
1654 assert!(last[2] > last[0], "gradient must end blue: {pixels:?}");
1655 }
1656
1657 #[test]
1658 fn svg_variant_cache_is_lru_bounded_and_observable() {
1659 let mut cache = SvgVariantCache::with_limits(2, 120);
1660 let mut first_canceled = None;
1661 for index in 0..3 {
1662 let source = SvgRasterSource::new(
1663 Arc::from(format!(
1664 "<svg width='1' height='1' data-index='{index}'>{}</svg>",
1665 "x".repeat(20)
1666 )),
1667 None,
1668 );
1669 let SvgVariantAccess::Start { canceled, .. } = cache.access(&source) else {
1670 panic!("new SVG source must start one worker");
1671 };
1672 if index == 0 {
1673 first_canceled = Some(canceled);
1674 }
1675 }
1676 let stats = cache.stats();
1677 assert!(stats.entries <= 2);
1678 assert_eq!(stats.misses, 3);
1679 assert!(stats.evictions >= 1);
1680 assert!(first_canceled.unwrap().load(Ordering::Acquire));
1681 assert!(stats.bytes <= stats.max_bytes);
1682 }
1683
1684 #[test]
1685 fn raster_decode_delivers_handle_on_foreground_drain() {
1686 let registry = AssetRegistry::new();
1687 registry
1688 .register(
1689 "app",
1690 InMemoryAssetProvider::new(BTreeMap::from([(
1691 "pixel".to_owned(),
1692 AssetData {
1693 mime_type: "image/png".to_owned(),
1694 bytes: one_pixel_png(),
1695 },
1696 )])),
1697 )
1698 .unwrap();
1699 let generation = ScriptGeneration::initial();
1700 registry
1701 .start_image_decode(
1702 &AssetId::parse("app/pixel").unwrap(),
1703 AsyncScope::Component(crate::ComponentInstancePath::root("App", "root")),
1704 generation,
1705 ScriptCallback::try_from_fn_ptr(FnPtr::new("loaded").unwrap(), generation).unwrap(),
1706 ScriptCallback::try_from_fn_ptr(FnPtr::new("failed").unwrap(), generation).unwrap(),
1707 )
1708 .unwrap();
1709 let mut deliveries = Vec::new();
1710 for _ in 0..1_000 {
1711 deliveries = registry.drain_image_decodes(generation).unwrap();
1712 if !deliveries.is_empty() {
1713 break;
1714 }
1715 std::thread::sleep(Duration::from_millis(1));
1716 }
1717 assert_eq!(deliveries.len(), 1);
1718 assert_eq!(deliveries[0].callback.name(), "loaded");
1719 let UiValue::Handle(handle) = &deliveries[0].payload else {
1720 panic!("successful decode must deliver an image handle");
1721 };
1722 registry.image_source(handle).unwrap();
1723 assert_eq!(registry.pending_decode_count(), 0);
1724 }
1725
1726 #[test]
1727 fn canceled_and_stale_image_decodes_do_not_deliver() {
1728 let registry = AssetRegistry::new();
1729 registry
1730 .register(
1731 "app",
1732 InMemoryAssetProvider::new(BTreeMap::from([(
1733 "pixel".to_owned(),
1734 AssetData {
1735 mime_type: "image/png".to_owned(),
1736 bytes: one_pixel_png(),
1737 },
1738 )])),
1739 )
1740 .unwrap();
1741 let generation = ScriptGeneration::initial();
1742 let handle = registry
1743 .start_image_decode(
1744 &AssetId::parse("app/pixel").unwrap(),
1745 AsyncScope::App,
1746 generation,
1747 ScriptCallback::try_from_fn_ptr(FnPtr::new("loaded").unwrap(), generation).unwrap(),
1748 ScriptCallback::try_from_fn_ptr(FnPtr::new("failed").unwrap(), generation).unwrap(),
1749 )
1750 .unwrap();
1751 assert!(registry.cancel_image_decode(handle).unwrap());
1752 let effect_scope = AsyncScope::Effect {
1753 component: crate::ComponentInstancePath::root("App", "root"),
1754 key: "image".to_owned(),
1755 activation: 1,
1756 };
1757 registry
1758 .start_image_decode(
1759 &AssetId::parse("app/pixel").unwrap(),
1760 effect_scope.clone(),
1761 generation,
1762 ScriptCallback::try_from_fn_ptr(FnPtr::new("loaded").unwrap(), generation).unwrap(),
1763 ScriptCallback::try_from_fn_ptr(FnPtr::new("failed").unwrap(), generation).unwrap(),
1764 )
1765 .unwrap();
1766 registry.cancel_scope(&effect_scope).unwrap();
1767 registry
1768 .retain_decode_generation(generation.next())
1769 .unwrap();
1770 std::thread::sleep(Duration::from_millis(10));
1771 assert!(
1772 registry
1773 .drain_image_decodes(generation.next())
1774 .unwrap()
1775 .is_empty()
1776 );
1777 assert_eq!(registry.pending_decode_count(), 0);
1778 }
1779
1780 #[test]
1781 fn transaction_rollback_restores_a_cancelled_decode_registration() {
1782 let registry = AssetRegistry::new();
1783 registry
1784 .register(
1785 "app",
1786 InMemoryAssetProvider::new(BTreeMap::from([(
1787 "pixel".to_owned(),
1788 AssetData {
1789 mime_type: "image/png".to_owned(),
1790 bytes: one_pixel_png(),
1791 },
1792 )])),
1793 )
1794 .unwrap();
1795 let generation = ScriptGeneration::initial();
1796 let handle = registry
1797 .start_image_decode(
1798 &AssetId::parse("app/pixel").unwrap(),
1799 AsyncScope::App,
1800 generation,
1801 ScriptCallback::try_from_fn_ptr(FnPtr::new("loaded").unwrap(), generation).unwrap(),
1802 ScriptCallback::try_from_fn_ptr(FnPtr::new("failed").unwrap(), generation).unwrap(),
1803 )
1804 .unwrap();
1805 let snapshot = registry.begin_transaction().unwrap();
1806 assert!(registry.cancel_image_decode(handle).unwrap());
1807 assert_eq!(registry.pending_decode_count(), 0);
1808 registry.restore_decode_snapshot(snapshot).unwrap();
1809 assert_eq!(registry.pending_decode_count(), 1);
1810 }
1811}