Skip to main content

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}