bevy_cache/config.rs
1use bevy::prelude::*;
2use std::path::{Component, Path, PathBuf};
3use std::time::Duration;
4
5/// Configuration for the file cache system.
6///
7/// The cache directory defaults to the operating system's cache folder
8/// (e.g. `%LOCALAPPDATA%` on Windows, `~/Library/Caches` on macOS,
9/// `$XDG_CACHE_HOME` on Linux) joined with the application name.
10/// On platforms where no cache directory can be determined (including
11/// some Android configurations), [`std::env::temp_dir`] is used as
12/// a fallback.
13#[derive(Debug, Clone, Resource)]
14pub struct CacheConfig {
15 /// Application name used to derive the manifest filename
16 /// (`"{app_name}.cache_manifest"`).
17 pub app_name: String,
18
19 /// Filesystem directory for cache files and the manifest.
20 pub cache_dir: PathBuf,
21
22 /// Maximum age of an entry before it becomes eligible for cleanup
23 /// at application exit.
24 pub max_age: Duration,
25
26 /// Maximum number of cache entries. `None` means unlimited.
27 /// Enforced at application exit — new entries are never rejected.
28 pub max_entries: Option<usize>,
29}
30
31impl CacheConfig {
32 /// Create a new config using the OS cache directory for the given app name.
33 pub fn new(app_name: &str) -> Self {
34 Self {
35 app_name: app_name.to_owned(),
36 cache_dir: resolve_cache_dir(app_name),
37 max_age: Duration::from_secs(604_800), // 7 days
38 max_entries: None,
39 }
40}
41
42 /// Returns the manifest filename, e.g. `"my_game.cache_manifest"`.
43 pub fn manifest_file_name(&self) -> String {
44 format!("{}.cache_manifest", self.app_name)
45 }
46
47 /// Filesystem path for a cached file.
48 pub fn file_path(&self, file_name: &str) -> PathBuf {
49 self.cache_dir.join(file_name)
50 }
51
52 /// Filesystem path for the manifest, e.g.
53 /// `<cache_dir>/my_game.cache_manifest`.
54 pub fn manifest_fs_path(&self) -> PathBuf {
55 self.cache_dir.join(self.manifest_file_name())
56 }
57
58 /// Ensure the cache directory exists on disk.
59 pub fn ensure_cache_dir(&self) -> Result<(), std::io::Error> {
60 std::fs::create_dir_all(&self.cache_dir)
61 }
62
63 /// Validate that a cache key is a safe relative path within the cache
64 /// root. Forward-slash subpaths are allowed, but traversal, absolute
65 /// paths, backslashes, empty segments, and manifest filename collisions
66 /// are rejected.
67 pub fn validate_key(&self, key: &str) -> Result<(), crate::CacheError> {
68 if key.is_empty()
69 || key.contains('\0')
70 || key.contains('\\')
71 || key.starts_with('/')
72 || key.ends_with('/')
73 || key.split('/').any(str::is_empty)
74 || key == self.manifest_file_name()
75 {
76 return Err(crate::CacheError::InvalidKey(key.to_owned()));
77 }
78
79 for component in Path::new(key).components() {
80 match component {
81 Component::Normal(_) => {}
82 Component::CurDir
83 | Component::ParentDir
84 | Component::RootDir
85 | Component::Prefix(_) => {
86 return Err(crate::CacheError::InvalidKey(key.to_owned()));
87 }
88 }
89 }
90
91 Ok(())
92 }
93}
94
95impl Default for CacheConfig {
96 fn default() -> Self {
97 Self::new("bevy_cache")
98 }
99}
100
101/// Resolve the platform-appropriate cache directory. Falls back to the OS temp
102/// dir when no user-specific cache folder is available (e.g. on Android).
103fn resolve_cache_dir(app_name: &str) -> PathBuf {
104 sysdirs::cache_dir()
105 .unwrap_or_else(std::env::temp_dir)
106 .join(app_name)
107}