1use std::{
27 any::{Any, TypeId},
28 collections::BTreeMap,
29 io::{self, Read},
30 path::{Path, PathBuf},
31 sync::{
32 Arc, Mutex, OnceLock, Weak,
33 atomic::{AtomicBool, AtomicUsize, Ordering},
34 },
35};
36
37use handle::Entry;
38
39pub mod environment;
40mod handle;
41mod pool;
42#[cfg(test)]
43mod tests;
44
45pub use environment::{FontEnvironment, FontIdentity};
46pub use handle::{AssetStatus, Handle};
47pub use pool::ResourcePool;
48
49const MAX_ENCODED: usize = 64 * 1024 * 1024;
50
51const MAX_CACHE_BYTES: usize = 256 * 1024 * 1024;
52
53const MAX_ENTRIES: usize = 4096;
54
55thread_local! {
56 static RESOLVING: std::cell::RefCell<Vec<usize>> = const { std::cell::RefCell::new(Vec::new()) };
57}
58
59#[derive(Clone, Default, Debug)]
62pub struct RequestDiagnostics(Arc<Mutex<Vec<Arc<crate::Error>>>>);
63
64impl RequestDiagnostics {
65 pub fn record(&self, error: Arc<crate::Error>) {
67 let mut errors = self
68 .0
69 .lock()
70 .unwrap_or_else(std::sync::PoisonError::into_inner);
71 if !errors.iter().any(|existing| Arc::ptr_eq(existing, &error)) {
72 errors.push(error);
73 }
74 }
75
76 pub fn errors(&self) -> Vec<Arc<crate::Error>> {
78 self.0
79 .lock()
80 .unwrap_or_else(std::sync::PoisonError::into_inner)
81 .clone()
82 }
83}
84
85struct ResolveGuard;
86
87impl Drop for ResolveGuard {
88 fn drop(&mut self) {
89 RESOLVING.with(|entries| {
90 entries.borrow_mut().pop();
91 });
92 }
93}
94
95pub trait Asset: Sized + Send + Sync + 'static {
97 const KIND: &'static str;
99
100 fn decode(bytes: &[u8], context: &LoadContext) -> crate::Result<Self>;
104
105 fn retained_bytes(&self) -> usize {
108 std::mem::size_of::<Self>()
109 }
110}
111
112#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
115pub enum Source {
116 File(PathBuf),
118 Bytes {
120 key: Arc<str>,
122 data: Arc<[u8]>,
124 },
125}
126
127impl std::fmt::Debug for Source {
128 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
129 match self {
130 Self::File(path) => formatter.debug_tuple("File").field(path).finish(),
131 Self::Bytes { key, data } => formatter
132 .debug_struct("Bytes")
133 .field("key", key)
134 .field("length", &data.len())
135 .finish(),
136 }
137 }
138}
139
140impl From<PathBuf> for Source {
141 fn from(path: PathBuf) -> Self {
142 Self::File(path)
143 }
144}
145
146impl From<&Path> for Source {
147 fn from(path: &Path) -> Self {
148 Self::File(path.to_path_buf())
149 }
150}
151
152impl From<&str> for Source {
153 fn from(path: &str) -> Self {
154 Self::File(path.into())
155 }
156}
157
158impl From<String> for Source {
159 fn from(path: String) -> Self {
160 Self::File(path.into())
161 }
162}
163
164#[derive(Clone, Debug)]
166pub struct LoadContext {
167 root: Arc<PathBuf>,
168 fonts: Option<crate::asset::environment::FontEnvironment>,
169 namespace: u64,
170}
171
172impl LoadContext {
173 pub fn fonts(&self) -> Option<&crate::asset::environment::FontEnvironment> {
175 self.fonts.as_ref()
176 }
177
178 pub fn root(&self) -> &Path {
180 &self.root
181 }
182
183 pub fn max_encoded_bytes(&self) -> usize {
185 MAX_ENCODED
186 }
187}
188
189type CacheKey = (TypeId, PathBuf, u64, u64, Source);
190
191type Erased = dyn Any + Send + Sync;
192
193struct OwnedEntry {
194 value: Arc<Erased>,
195 initialized: Box<dyn Fn() -> bool + Send + Sync>,
196}
197
198impl OwnedEntry {
199 fn new<AssetValue: Asset>(entry: Arc<Entry<AssetValue>>) -> Arc<Self> {
200 let weak = Arc::downgrade(&entry);
201 Arc::new(Self {
202 value: entry,
203 initialized: Box::new(move || {
204 weak.upgrade()
205 .is_some_and(|entry| entry.result.get().is_some())
206 }),
207 })
208 }
209}
210
211#[derive(Clone)]
215pub struct Assets {
216 context: LoadContext,
217 entries: Arc<Mutex<BTreeMap<CacheKey, Arc<OwnedEntry>>>>,
218 pool: ResourcePool,
219 diagnostics: Option<RequestDiagnostics>,
220}
221
222impl std::fmt::Debug for Assets {
223 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
224 formatter
225 .debug_struct("Assets")
226 .field("root", &self.context.root)
227 .finish_non_exhaustive()
228 }
229}
230
231impl Default for Assets {
232 fn default() -> Self {
233 Self {
234 context: LoadContext {
235 root: Arc::new(PathBuf::new()),
236 fonts: None,
237 namespace: 0,
238 },
239 entries: Arc::new(Mutex::new(BTreeMap::new())),
240 pool: ResourcePool::new(),
241 diagnostics: None,
242 }
243 }
244}
245
246impl Assets {
247 pub fn with_diagnostics(mut self, diagnostics: RequestDiagnostics) -> Self {
250 self.diagnostics = Some(diagnostics);
251 self
252 }
253
254 pub fn snapshot(&self) -> Self {
257 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
258 let mut snapshot = self.clone();
259 snapshot.context.namespace = NEXT.fetch_add(1, Ordering::Relaxed);
260 let entries = self
261 .entries
262 .lock()
263 .unwrap_or_else(std::sync::PoisonError::into_inner)
264 .iter()
265 .filter(|(_, entry)| (entry.initialized)())
266 .map(|(key, entry)| {
267 let mut key = key.clone();
268 key.3 = snapshot.context.namespace;
269 (key, entry.clone())
270 })
271 .collect();
272 snapshot.entries = Arc::new(Mutex::new(entries));
273 snapshot
274 }
275
276 pub fn preload<A: Asset>(
279 &self,
280 source: impl Into<Source>,
281 value: Arc<A>,
282 ) -> crate::Result<Handle<A>> {
283 let handle = self.load::<A>(source);
284 let inserted = std::cell::Cell::new(false);
285 let result = handle.entry.result.get_or_init(|| {
286 inserted.set(true);
287 let charge = value
288 .retained_bytes()
289 .checked_add(handle.entry.charged.load(Ordering::Acquire));
290 let result = charge
291 .ok_or_else(|| {
292 io::Error::new(
293 io::ErrorKind::OutOfMemory,
294 "Asset payload accounting overflow",
295 )
296 })
297 .and_then(|charge| {
298 let additional = charge - handle.entry.charged.load(Ordering::Acquire);
299 self.pool
300 .0
301 .bytes
302 .try_update(Ordering::AcqRel, Ordering::Acquire, |current| {
303 current
304 .checked_add(additional)
305 .filter(|total| *total <= self.pool.0.max_bytes)
306 })
307 .map_err(|_| {
308 io::Error::new(
309 io::ErrorKind::OutOfMemory,
310 "Shared asset-cache logical payload exceeds its allowance",
311 )
312 })?;
313 handle.entry.charged.store(charge, Ordering::Release);
314 Ok(value)
315 });
316 result.map_err(|error| {
317 Arc::new(crate::Error::asset(A::KIND, handle.source_name(), error))
318 })
319 });
320 if let Err(error) = result {
321 return Err(crate::Error::asset(
322 A::KIND,
323 handle.source_name(),
324 error.clone(),
325 ));
326 }
327 if !inserted.get() {
328 return Err(io::Error::new(
329 io::ErrorKind::AlreadyExists,
330 "Asset source is already resolved",
331 )
332 .into());
333 }
334 Ok(handle)
335 }
336
337 pub fn with_pool(mut self, pool: ResourcePool) -> Self {
340 self.pool = pool;
341 self.entries = Arc::new(Mutex::new(BTreeMap::new()));
342 self
343 }
344
345 pub fn pool(&self) -> &ResourcePool {
347 &self.pool
348 }
349
350 pub fn remove<A: Asset>(&self, source: impl Into<Source>) -> bool {
354 let mut source = source.into();
355 if let Source::File(path) = &mut source
356 && !path.is_absolute()
357 {
358 *path = self.context.root.join(&*path);
359 }
360 let key = (
361 TypeId::of::<A>(),
362 self.context.root.as_ref().clone(),
363 self.context
364 .fonts
365 .as_ref()
366 .map_or(0, crate::asset::environment::FontEnvironment::snapshot_id),
367 self.context.namespace,
368 source,
369 );
370 let mut entries = self
371 .entries
372 .lock()
373 .unwrap_or_else(std::sync::PoisonError::into_inner);
374 let Some(entry) = entries.remove(&key) else {
375 return false;
376 };
377 let mut global = self
378 .pool
379 .0
380 .entries
381 .lock()
382 .unwrap_or_else(std::sync::PoisonError::into_inner);
383 if global
384 .get(&key)
385 .and_then(Weak::upgrade)
386 .is_some_and(|value| Arc::ptr_eq(&value, &entry.value))
387 {
388 global.remove(&key);
389 }
390 true
391 }
392
393 pub fn with_fonts(
396 mut self,
397 fonts: impl Into<crate::asset::environment::FontEnvironment>,
398 ) -> Self {
399 self.context.fonts = Some(fonts.into());
400 self.entries = Arc::new(Mutex::new(BTreeMap::new()));
401 self
402 }
403
404 pub fn fonts(&self) -> Option<&crate::asset::environment::FontEnvironment> {
406 self.context.fonts()
407 }
408
409 pub fn new(root: impl AsRef<Path>) -> crate::Result<Self> {
411 Ok(Self {
412 context: LoadContext {
413 root: Arc::new(std::path::absolute(root)?),
414 fonts: None,
415 namespace: 0,
416 },
417 entries: Arc::new(Mutex::new(BTreeMap::new())),
418 pool: ResourcePool::new(),
419 diagnostics: None,
420 })
421 }
422
423 pub fn load<A: Asset>(&self, source: impl Into<Source>) -> Handle<A> {
427 let mut source = source.into();
428 let failure = if let Source::File(path) = &mut source {
429 if self.context.root.as_os_str().is_empty() && !path.is_absolute() {
430 Some("Relative file assets require an explicit project root")
431 } else if path
432 .components()
433 .any(|component| component == std::path::Component::ParentDir)
434 {
435 Some("Asset paths must not contain parent traversal")
436 } else {
437 if !path.is_absolute() {
438 *path = self.context.root.join(&*path);
439 }
440 None
441 }
442 } else if let Source::Bytes { key, data } = &source {
443 if data.len() > MAX_ENCODED {
444 Some("Asset source exceeds 64 MiB")
445 } else if key.is_empty() || key.len() > 1024 || key.chars().any(char::is_control) {
446 Some("Asset byte keys require 1..=1024 bytes without control characters")
447 } else {
448 None
449 }
450 } else {
451 None
452 };
453 let key = (
454 TypeId::of::<A>(),
455 self.context.root.as_ref().clone(),
456 self.context
457 .fonts
458 .as_ref()
459 .map_or(0, crate::asset::environment::FontEnvironment::snapshot_id),
460 self.context.namespace,
461 source.clone(),
462 );
463 let mut entries = self
464 .entries
465 .lock()
466 .unwrap_or_else(std::sync::PoisonError::into_inner);
467 if failure.is_none()
468 && let Some(entry) = entries.get(&key)
469 {
470 return Handle {
471 entry: entry
472 .value
473 .clone()
474 .downcast::<Entry<A>>()
475 .expect("typed resource key"),
476 diagnostics: self.diagnostics.clone(),
477 };
478 }
479 let mut global = self
480 .pool
481 .0
482 .entries
483 .lock()
484 .unwrap_or_else(std::sync::PoisonError::into_inner);
485 global.retain(|_, entry| entry.strong_count() > 0);
486 let failure = failure.or_else(|| {
487 (entries.len() >= self.pool.0.max_entries)
488 .then_some("Resource cache exceeds its entry allowance")
489 });
490 if failure.is_none()
491 && let Some(entry) = global.get(&key).and_then(Weak::upgrade)
492 {
493 let entry = entry.downcast::<Entry<A>>().expect("typed global key");
494 entries.insert(key, OwnedEntry::new(entry.clone()));
495 return Handle {
496 entry,
497 diagnostics: self.diagnostics.clone(),
498 };
499 }
500 let mut failure = failure.or_else(|| {
501 (global.len() >= self.pool.0.max_entries)
502 .then_some("Resource cache exceeds its entry allowance")
503 });
504 let mut charged = 0;
505 if failure.is_none()
506 && let Source::Bytes { data, .. } = &source
507 {
508 if self
509 .pool
510 .0
511 .bytes
512 .try_update(Ordering::AcqRel, Ordering::Acquire, |current| {
513 current
514 .checked_add(data.len())
515 .filter(|total| *total <= self.pool.0.max_bytes)
516 })
517 .is_ok()
518 {
519 charged = data.len();
520 } else {
521 failure = Some("Shared asset-cache logical payload exceeds its allowance");
522 }
523 }
524 if failure.is_some()
525 && let Source::Bytes { data, .. } = &mut source
526 {
527 *data = Arc::from([]);
528 }
529 let entry = Arc::new(Entry {
530 pool: self.pool.clone(),
531 source,
532 context: self.context.clone(),
533 result: OnceLock::new(),
534 loading: AtomicBool::new(false),
535 charged: AtomicUsize::new(charged),
536 });
537 if let Some(message) = failure {
538 let source_name = match &entry.source {
539 Source::File(path) => path.display().to_string(),
540 Source::Bytes { key, .. } => key.to_string(),
541 };
542 let _result = entry.result.set(Err(Arc::new(crate::Error::asset(
543 A::KIND,
544 source_name,
545 io::Error::new(io::ErrorKind::InvalidInput, message),
546 ))));
547 } else {
548 let erased: Arc<Erased> = entry.clone();
549 global.insert(key.clone(), Arc::downgrade(&erased));
550 entries.insert(key, OwnedEntry::new(entry.clone()));
551 }
552 Handle {
553 entry,
554 diagnostics: self.diagnostics.clone(),
555 }
556 }
557}