use std::{
fs::{create_dir_all, remove_dir_all},
path::{Path, PathBuf},
};
use crate::common::{CvmfsError, CvmfsResult};
#[derive(Debug, Clone)]
pub struct Cache {
pub cache_directory: String,
}
impl Cache {
pub fn new(cache_directory: String) -> CvmfsResult<Self> {
let path = Path::new(&cache_directory);
Ok(Self { cache_directory: path.to_str().ok_or(CvmfsError::FileNotFound)?.into() })
}
pub fn initialize(&self) -> CvmfsResult<()> {
let base_path = self.create_directory("data")?;
for i in 0x00..=0xff {
let new_folder = format!("{:02x}", i);
let new_file = Path::join::<&Path>(base_path.as_ref(), new_folder.as_ref());
create_dir_all(new_file)?;
}
Ok(())
}
fn create_directory(&self, path: &str) -> CvmfsResult<String> {
let cache_full_path = Path::new(&self.cache_directory).join(path);
create_dir_all(cache_full_path.clone())?;
cache_full_path
.into_os_string()
.into_string()
.map_err(|_| CvmfsError::FileNotFound)
}
pub fn add(&self, file_name: &str) -> CvmfsResult<PathBuf> {
if file_name.contains("..") || file_name.starts_with('/') {
return Err(CvmfsError::IO("invalid cache filename".to_string()));
}
let path = Path::join(self.cache_directory.as_ref(), file_name);
Ok(path)
}
pub fn get(&self, file_name: &str) -> Option<PathBuf> {
let path = self.add(file_name).ok()?;
if path.is_file() {
return Some(path);
}
None
}
pub fn evict(&self) -> CvmfsResult<()> {
let data_path = Path::new(&self.cache_directory).join("data");
if data_path.exists() && data_path.is_dir() {
remove_dir_all(data_path)?;
self.initialize()?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
fn tmp_cache_dir(name: &str) -> PathBuf {
std::env::temp_dir().join(format!("cvmfs_cache_{}_{}", name, std::process::id()))
}
#[test]
fn cache_new_valid_path() {
let dir = tmp_cache_dir("new");
fs::create_dir_all(&dir).unwrap();
let cache = Cache::new(dir.to_str().unwrap().into()).unwrap();
assert_eq!(cache.cache_directory, dir.to_str().unwrap());
fs::remove_dir_all(&dir).ok();
}
#[test]
fn cache_initialize_creates_data_subdirs() {
let dir = tmp_cache_dir("init");
fs::create_dir_all(&dir).unwrap();
let cache = Cache::new(dir.to_str().unwrap().into()).unwrap();
cache.initialize().unwrap();
let data_dir = dir.join("data");
assert!(data_dir.is_dir());
assert!(data_dir.join("00").is_dir());
assert!(data_dir.join("0a").is_dir());
assert!(data_dir.join("ff").is_dir());
assert!(data_dir.join("7f").is_dir());
let count = fs::read_dir(&data_dir).unwrap().count();
assert_eq!(count, 256);
fs::remove_dir_all(&dir).ok();
}
#[test]
fn cache_add_normal_filename() {
let dir = tmp_cache_dir("add");
fs::create_dir_all(&dir).unwrap();
let cache = Cache::new(dir.to_str().unwrap().into()).unwrap();
let result = cache.add("data/ab/cdef1234").unwrap();
assert_eq!(result, dir.join("data/ab/cdef1234"));
fs::remove_dir_all(&dir).ok();
}
#[test]
fn cache_add_path_traversal_rejected() {
let dir = tmp_cache_dir("traversal");
fs::create_dir_all(&dir).unwrap();
let cache = Cache::new(dir.to_str().unwrap().into()).unwrap();
let result = cache.add("../etc/passwd");
assert!(result.is_err());
fs::remove_dir_all(&dir).ok();
}
#[test]
fn cache_add_absolute_path_rejected() {
let dir = tmp_cache_dir("absolute");
fs::create_dir_all(&dir).unwrap();
let cache = Cache::new(dir.to_str().unwrap().into()).unwrap();
let result = cache.add("/etc/passwd");
assert!(result.is_err());
fs::remove_dir_all(&dir).ok();
}
#[test]
fn cache_get_missing_file_returns_none() {
let dir = tmp_cache_dir("get_miss");
fs::create_dir_all(&dir).unwrap();
let cache = Cache::new(dir.to_str().unwrap().into()).unwrap();
cache.initialize().unwrap();
assert!(cache.get("data/ab/nonexistent").is_none());
fs::remove_dir_all(&dir).ok();
}
#[test]
fn cache_get_existing_file_returns_some() {
let dir = tmp_cache_dir("get_hit");
fs::create_dir_all(&dir).unwrap();
let cache = Cache::new(dir.to_str().unwrap().into()).unwrap();
cache.initialize().unwrap();
let file_path = dir.join("data/ab/testfile");
fs::write(&file_path, b"content").unwrap();
let result = cache.get("data/ab/testfile");
assert!(result.is_some());
assert_eq!(result.unwrap(), file_path);
fs::remove_dir_all(&dir).ok();
}
#[test]
fn cache_evict_clears_and_recreates() {
let dir = tmp_cache_dir("evict");
fs::create_dir_all(&dir).unwrap();
let cache = Cache::new(dir.to_str().unwrap().into()).unwrap();
cache.initialize().unwrap();
let file_path = dir.join("data/ab/toevict");
fs::write(&file_path, b"data").unwrap();
assert!(file_path.is_file());
cache.evict().unwrap();
assert!(!file_path.is_file());
assert!(dir.join("data").is_dir());
assert!(dir.join("data/ab").is_dir());
fs::remove_dir_all(&dir).ok();
}
}