Skip to main content

a3s_box_runtime/cache/
mod.rs

1//! Cache module for cold start optimization.
2//!
3//! Provides two caching layers:
4//! - `LayerCache`: Content-addressed cache for extracted OCI layers
5//! - `RootfsCache`: Cache for fully-built rootfs directories
6
7use std::path::{Component, Path};
8
9use a3s_box_core::error::{BoxError, Result};
10
11pub mod layer_cache;
12pub mod rootfs_cache;
13
14pub use layer_cache::LayerCache;
15pub use rootfs_cache::{prune_apfs_rootfs_cache_all, RootfsCache, RootfsPruneResult};
16
17/// Validate a caller-provided cache key before it is appended to a host path.
18///
19/// Cache keys are identifiers, not relative paths. Keeping this check in one
20/// place prevents a malformed OCI digest or API key from escaping the cache
21/// directory through `..`, path separators, or platform-specific prefixes.
22pub(crate) fn validate_cache_key(key: &str, kind: &str) -> Result<()> {
23    if key.is_empty() || key.contains('\0') || key.contains('/') || key.contains('\\') {
24        return Err(BoxError::CacheError(format!(
25            "Invalid {kind} cache key: expected a single path component"
26        )));
27    }
28
29    let mut components = Path::new(key).components();
30    if !matches!(components.next(), Some(Component::Normal(_))) || components.next().is_some() {
31        return Err(BoxError::CacheError(format!(
32            "Invalid {kind} cache key: expected a single path component"
33        )));
34    }
35
36    Ok(())
37}
38
39/// Return whether `path` is an actual directory, rather than a symlink to one.
40///
41/// Cache paths are later handed to mount/copy code, so following a symlink here
42/// would turn a local cache entry into an arbitrary host path.
43pub(crate) fn is_real_directory(path: &Path) -> bool {
44    std::fs::symlink_metadata(path)
45        .map(|metadata| metadata.is_dir() && !metadata.file_type().is_symlink())
46        .unwrap_or(false)
47}
48
49/// Return whether `path` is an actual regular file, rather than a symlink.
50pub(crate) fn is_regular_file(path: &Path) -> bool {
51    std::fs::symlink_metadata(path)
52        .map(|metadata| metadata.is_file() && !metadata.file_type().is_symlink())
53        .unwrap_or(false)
54}
55
56/// Remove one cache path without following a symlink at the path itself.
57pub(crate) fn remove_path_no_follow(path: &Path) -> Result<()> {
58    let metadata = match std::fs::symlink_metadata(path) {
59        Ok(metadata) => metadata,
60        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
61        Err(error) => {
62            return Err(BoxError::CacheError(format!(
63                "Failed to inspect cached path {}: {error}",
64                path.display()
65            )))
66        }
67    };
68    let removed = if metadata.is_dir() && !metadata.file_type().is_symlink() {
69        std::fs::remove_dir_all(path)
70    } else {
71        std::fs::remove_file(path)
72    };
73    match removed {
74        Ok(()) => Ok(()),
75        Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
76        Err(error) => Err(BoxError::CacheError(format!(
77            "Failed to remove cached path {}: {error}",
78            path.display()
79        ))),
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    #[test]
88    fn cache_keys_must_be_single_path_components() {
89        for key in ["", ".", "..", "../escape", "nested/key", "nested\\key"] {
90            assert!(validate_cache_key(key, "test").is_err(), "key={key:?}");
91        }
92
93        for key in ["sha256_abc123", "rootfs-key", "键"] {
94            assert!(validate_cache_key(key, "test").is_ok(), "key={key:?}");
95        }
96    }
97}