use crate::backend::memory::{MemoryBackend, MemoryBackendBuilder};
use crate::backend::{CacheBackend, TieredBackend};
use crate::error::{CacheError, Result};
use crate::utils::redaction::redact_value;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tracing::instrument;
#[derive(Debug, Clone)]
pub struct PathValidationConfig {
pub allowed_base_dirs: Vec<PathBuf>,
pub allow_symbolic_links: bool,
pub max_path_length: usize,
}
impl Default for PathValidationConfig {
fn default() -> Self {
Self {
allowed_base_dirs: Vec::new(),
allow_symbolic_links: false,
max_path_length: 4096,
}
}
}
impl PathValidationConfig {
pub fn new() -> Self {
Self::default()
}
pub fn add_allowed_base_dir(mut self, dir: impl Into<PathBuf>) -> Self {
self.allowed_base_dirs.push(dir.into());
self
}
pub fn allow_symbolic_links(mut self, allowed: bool) -> Self {
self.allow_symbolic_links = allowed;
self
}
pub fn with_max_path_length(mut self, length: usize) -> Self {
self.max_path_length = length;
self
}
pub fn validate(&self, path: &str) -> Result<PathBuf> {
if path.len() > self.max_path_length {
return Err(CacheError::ConfigError(format!(
"Path exceeds maximum length of {} characters",
self.max_path_length
)));
}
let path = Path::new(path);
if !path.is_absolute() {
return Err(CacheError::ConfigError(
"Only absolute paths are allowed".to_string(),
));
}
let normalized = match path.canonicalize() {
Ok(p) => p,
Err(_) => {
let mut buf = PathBuf::new();
for component in path.components() {
match component {
std::path::Component::Normal(part) => {
buf.push(part);
}
std::path::Component::CurDir => {} std::path::Component::ParentDir => {
if !buf.pop() {
return Err(CacheError::ConfigError(
"Path traversal attempt detected".to_string(),
));
}
}
_ => {}
}
}
buf
}
};
if !self.allowed_base_dirs.is_empty() {
let mut within_allowed = false;
for base_dir in &self.allowed_base_dirs {
let base_canonical = match base_dir.canonicalize() {
Ok(p) => p,
Err(_) => continue,
};
if normalized.starts_with(&base_canonical) {
within_allowed = true;
break;
}
}
if !within_allowed {
return Err(CacheError::ConfigError(format!(
"Path is not within allowed directories: {}",
normalized.display()
)));
}
}
if !self.allow_symbolic_links {
if let Some(file_name) = normalized.file_name() {
if file_name.to_string_lossy().starts_with('.') {
tracing::warn!("Loading configuration from hidden file: {}", path.display());
}
}
}
validate_path_chars(path)?;
Ok(normalized)
}
}
fn validate_path_chars(path: &Path) -> Result<()> {
let invalid_chars = ['\0', '\n', '\r', '\t'];
let path_str = path.to_string_lossy();
for ch in invalid_chars {
if path_str.contains(ch) {
return Err(CacheError::ConfigError(format!(
"Path contains invalid character: {:?}",
ch
)));
}
}
Ok(())
}
#[derive(Debug, Clone, Copy)]
pub struct ConfigValidation;
impl ConfigValidation {
pub const MAX_CAPACITY: u64 = 1_000_000_000;
pub const MAX_TTL_SECS: u64 = 30 * 24 * 60 * 60;
pub const MAX_TTI_SECS: u64 = 30 * 24 * 60 * 60;
pub const MAX_CUSTOM_NAME_LENGTH: usize = 256;
pub const VALID_NAME_CHARS: &'static str =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_.-";
pub fn validate_capacity(capacity: u64) -> Result<u64> {
if capacity == 0 {
return Err(CacheError::ConfigError(
"Capacity must be greater than 0".to_string(),
));
}
if capacity > Self::MAX_CAPACITY {
return Err(CacheError::ConfigError(format!(
"Capacity {} exceeds maximum allowed value of {}",
capacity,
Self::MAX_CAPACITY
)));
}
Ok(capacity)
}
pub fn validate_ttl(ttl: u64) -> Result<u64> {
if ttl == 0 {
return Err(CacheError::ConfigError(
"TTL must be greater than 0".to_string(),
));
}
if ttl > Self::MAX_TTL_SECS {
return Err(CacheError::ConfigError(format!(
"TTL {} seconds exceeds maximum allowed value of {} seconds (30 days)",
ttl,
Self::MAX_TTL_SECS
)));
}
Ok(ttl)
}
pub fn validate_tti(tti: u64) -> Result<u64> {
if tti > Self::MAX_TTI_SECS {
return Err(CacheError::ConfigError(format!(
"Time to idle {} seconds exceeds maximum allowed value of {} seconds (30 days)",
tti,
Self::MAX_TTI_SECS
)));
}
Ok(tti)
}
pub fn validate_custom_name(name: &str) -> Result<String> {
if name.is_empty() {
return Err(CacheError::ConfigError(
"Custom backend name cannot be empty".to_string(),
));
}
if name.len() > Self::MAX_CUSTOM_NAME_LENGTH {
return Err(CacheError::ConfigError(format!(
"Custom backend name exceeds maximum length of {} characters",
Self::MAX_CUSTOM_NAME_LENGTH
)));
}
for ch in name.chars() {
if !Self::VALID_NAME_CHARS.contains(ch) {
return Err(CacheError::ConfigError(format!(
"Custom backend name contains invalid character '{}'. Allowed characters: {}",
ch,
Self::VALID_NAME_CHARS
)));
}
}
Ok(name.to_string())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
pub enum Layer {
#[default]
L1,
L2,
}
impl fmt::Display for Layer {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Layer::L1 => write!(f, "L1"),
Layer::L2 => write!(f, "L2"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LayerRestriction {
L1Only,
L2Only,
Any,
}
impl LayerRestriction {
pub fn supports(&self, layer: Layer) -> bool {
match self {
LayerRestriction::L1Only => layer == Layer::L1,
LayerRestriction::L2Only => layer == Layer::L2,
LayerRestriction::Any => true,
}
}
pub fn description(&self) -> &'static str {
match self {
LayerRestriction::L1Only => "仅支持 L1 层级",
LayerRestriction::L2Only => "仅支持 L2 层级",
LayerRestriction::Any => "支持任意层级",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum BackendType {
#[cfg(feature = "l1-moka")]
Moka,
#[cfg(not(feature = "l1-moka"))]
Memory,
#[cfg(feature = "l2-redis")]
Redis,
#[default]
Tiered,
Persisted,
Custom(String),
}
impl fmt::Display for BackendType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
#[cfg(feature = "l1-moka")]
BackendType::Moka => write!(f, "moka"),
#[cfg(not(feature = "l1-moka"))]
BackendType::Memory => write!(f, "memory"),
#[cfg(feature = "l2-redis")]
BackendType::Redis => write!(f, "redis"),
BackendType::Tiered => write!(f, "tiered"),
BackendType::Persisted => write!(f, "persisted"),
BackendType::Custom(name) => {
let masked = redact_value(name, 8);
write!(f, "custom:{}", masked)
}
}
}
}
impl BackendType {
pub fn layer_restriction(&self) -> LayerRestriction {
match self {
#[cfg(feature = "l1-moka")]
BackendType::Moka => LayerRestriction::L1Only,
#[cfg(not(feature = "l1-moka"))]
BackendType::Memory => LayerRestriction::L1Only,
#[cfg(feature = "l2-redis")]
BackendType::Redis => LayerRestriction::L2Only,
BackendType::Tiered => LayerRestriction::Any,
BackendType::Persisted => LayerRestriction::L2Only,
BackendType::Custom(_) => LayerRestriction::Any,
}
}
pub fn recommended_layer(&self) -> Layer {
match self.layer_restriction() {
LayerRestriction::L1Only => Layer::L1,
LayerRestriction::L2Only => Layer::L2,
LayerRestriction::Any => Layer::L1,
}
}
pub fn supports_layer(&self, layer: Layer) -> bool {
self.layer_restriction().supports(layer)
}
pub fn available_backends() -> Vec<BackendType> {
vec![
#[cfg(feature = "l1-moka")]
BackendType::Moka,
#[cfg(feature = "l2-redis")]
BackendType::Redis,
BackendType::Tiered,
BackendType::Persisted,
]
}
#[allow(clippy::should_implement_trait)]
pub fn from_str(s: &str) -> Result<Self> {
match s.to_lowercase().as_str() {
#[cfg(feature = "l1-moka")]
"moka" => Ok(BackendType::Moka),
#[cfg(not(feature = "l1-moka"))]
"memory" | "moka" => Ok(BackendType::Memory),
#[cfg(feature = "l2-redis")]
"redis" => Ok(BackendType::Redis),
"tiered" | "multi" | "two-level" => Ok(BackendType::Tiered),
"persisted" | "persist" | "sqlite" => Ok(BackendType::Persisted),
_ => {
if let Some(custom_name) = s.strip_prefix("custom:") {
let validated_name = ConfigValidation::validate_custom_name(custom_name)?;
Ok(BackendType::Custom(validated_name))
} else {
#[cfg(feature = "l1-moka")]
#[cfg(feature = "l2-redis")]
let available = "moka, memory, redis, tiered, persisted, custom:<name>";
#[cfg(feature = "l1-moka")]
#[cfg(not(feature = "l2-redis"))]
let available = "moka, memory, tiered, persisted, custom:<name>";
#[cfg(not(feature = "l1-moka"))]
#[cfg(feature = "l2-redis")]
let available = "memory, redis, tiered, persisted, custom:<name>";
#[cfg(not(feature = "l1-moka"))]
#[cfg(not(feature = "l2-redis"))]
let available = "memory, tiered, persisted, custom:<name>";
Err(CacheError::ConfigError(format!(
"Unknown backend type: '{}'. Available backends: {}",
s, available
)))
}
}
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct LayerBackendConfig {
#[serde(default)]
pub backend_type: BackendType,
#[serde(default)]
pub options: serde_json::Value,
#[serde(default = "default_enabled")]
pub enabled: bool,
}
fn default_enabled() -> bool {
true
}
impl LayerBackendConfig {
pub fn new(backend_type: BackendType) -> Self {
Self {
backend_type,
options: serde_json::Value::Null,
enabled: true,
}
}
pub fn with_backend_type(mut self, backend_type: BackendType) -> Self {
self.backend_type = backend_type;
self
}
pub fn with_options(mut self, options: serde_json::Value) -> Self {
self.options = options;
self
}
pub fn with_enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
pub fn validate(&self, layer: Layer) -> Result<()> {
if !self.enabled {
return Ok(());
}
if !self.backend_type.supports_layer(layer) {
return Err(CacheError::ConfigError(format!(
"Backend type '{}' does not support layer {}. {}",
self.backend_type,
layer,
self.backend_type.layer_restriction().description()
)));
}
Ok(())
}
}
#[async_trait]
pub trait BackendProvider: Send + Sync {
async fn create_l1(&self, options: &serde_json::Value) -> Result<Arc<dyn CacheBackend>>;
async fn create_l2(&self, options: &serde_json::Value) -> Result<Arc<dyn CacheBackend>>;
}
#[derive(Default)]
pub struct DefaultBackendProvider;
#[async_trait]
impl BackendProvider for DefaultBackendProvider {
#[instrument(skip(self, options), level = "debug")]
async fn create_l1(&self, options: &serde_json::Value) -> Result<Arc<dyn CacheBackend>> {
let mut builder = MemoryBackend::builder();
builder = apply_memory_options(builder, options);
let backend = builder.build();
Ok(Arc::new(backend))
}
#[instrument(skip(self, options), level = "debug")]
async fn create_l2(&self, options: &serde_json::Value) -> Result<Arc<dyn CacheBackend>> {
#[cfg(feature = "l2-redis")]
{
use crate::backend::RedisBackend;
let connection_string = options
.get("connection_string")
.and_then(|v| v.as_str())
.unwrap_or("redis://localhost:6379");
tracing::debug!(
"Creating Redis backend with connection: redis://***@{}",
connection_string
.split('@')
.nth(1)
.unwrap_or(connection_string)
.split(':')
.next()
.unwrap_or("unknown")
);
let backend = Arc::new(RedisBackend::new(connection_string).await?);
Ok(backend)
}
#[cfg(not(feature = "l2-redis"))]
{
tracing::warn!("Redis backend not available, falling back to memory backend");
let mut builder = MemoryBackend::builder();
builder = apply_memory_options(builder, options);
let backend = builder.build();
Ok(Arc::new(backend))
}
}
}
fn apply_memory_options(
mut builder: MemoryBackendBuilder,
options: &serde_json::Value,
) -> MemoryBackendBuilder {
if let Some(options) = options.as_object() {
if let Some(capacity) = options.get("capacity").and_then(|v| v.as_u64()) {
match ConfigValidation::validate_capacity(capacity) {
Ok(validated) => {
builder = builder.capacity(validated);
}
Err(e) => {
tracing::warn!("Invalid capacity: {}", e);
}
}
}
if let Some(ttl) = options.get("ttl").and_then(|v| v.as_u64()) {
match ConfigValidation::validate_ttl(ttl) {
Ok(validated) => {
builder = builder.ttl(std::time::Duration::from_secs(validated));
}
Err(e) => {
tracing::warn!("Invalid TTL: {}", e);
}
}
}
if let Some(tti) = options.get("time_to_idle").and_then(|v| v.as_u64()) {
match ConfigValidation::validate_tti(tti) {
Ok(validated) => {
builder = builder.time_to_idle(std::time::Duration::from_secs(validated));
}
Err(e) => {
tracing::warn!("Invalid TTI: {}", e);
}
}
}
}
builder
}
#[derive(Clone)]
pub struct TieredBackendFactory {
provider: Arc<dyn BackendProvider>,
}
impl Default for TieredBackendFactory {
fn default() -> Self {
Self::new()
}
}
impl TieredBackendFactory {
pub fn new() -> Self {
Self {
provider: Arc::new(DefaultBackendProvider),
}
}
pub fn with_provider(provider: Arc<dyn BackendProvider>) -> Self {
Self { provider }
}
#[instrument(skip(self, options), level = "debug")]
pub async fn create_l1(&self, options: &serde_json::Value) -> Result<Arc<dyn CacheBackend>> {
self.provider.create_l1(options).await
}
#[instrument(skip(self, options), level = "debug")]
pub async fn create_l2(&self, options: &serde_json::Value) -> Result<Arc<dyn CacheBackend>> {
self.provider.create_l2(options).await
}
#[instrument(skip(self, l1_options, l2_options), level = "debug")]
pub async fn create_tiered_backend(
&self,
l1_options: &serde_json::Value,
l2_options: &serde_json::Value,
) -> Result<TieredBackend> {
let l1 = self.create_l1(l1_options).await?;
let l2 = self.create_l2(l2_options).await?;
Ok(TieredBackend::from_arc(l1, l2))
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct CustomTieredConfig {
pub l1: LayerBackendConfig,
pub l2: LayerBackendConfig,
#[serde(default)]
pub auto_fix: AutoFixConfig,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct AutoFixConfig {
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(default = "default_true")]
pub warn_on_fix: bool,
}
impl Default for AutoFixConfig {
fn default() -> Self {
Self {
enabled: true,
warn_on_fix: true,
}
}
}
fn default_true() -> bool {
true
}
impl AutoFixConfig {
pub fn new() -> Self {
Self {
enabled: true,
warn_on_fix: true,
}
}
pub fn with_enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
pub fn with_warn_on_fix(mut self, warn: bool) -> Self {
self.warn_on_fix = warn;
self
}
}
impl CustomTieredConfig {
#[cfg(feature = "l1-moka")]
pub fn new() -> Self {
Self {
l1: LayerBackendConfig::new(BackendType::Moka),
l2: LayerBackendConfig::new(BackendType::Redis),
auto_fix: AutoFixConfig::new(),
}
}
#[cfg(not(feature = "l1-moka"))]
pub fn new() -> Self {
Self {
l1: LayerBackendConfig::new(BackendType::Memory),
l2: LayerBackendConfig::new(BackendType::Tiered),
auto_fix: AutoFixConfig::new(),
}
}
pub fn l1_backend(mut self, backend_type: BackendType) -> Self {
self.l1.backend_type = backend_type;
self
}
pub fn l2_backend(mut self, backend_type: BackendType) -> Self {
self.l2.backend_type = backend_type;
self
}
pub fn auto_fix(mut self, enabled: bool) -> Self {
self.auto_fix.enabled = enabled;
self
}
pub fn validate(&self) -> ConfigValidationResult {
let mut result = ConfigValidationResult::new();
if self.l1.enabled {
match self.l1.validate(Layer::L1) {
Ok(_) => {
result.add_valid(Layer::L1, self.l1.backend_type.clone());
}
Err(e) => {
result.add_invalid(Layer::L1, self.l1.backend_type.clone(), e.to_string());
}
}
}
if self.l2.enabled {
match self.l2.validate(Layer::L2) {
Ok(_) => {
result.add_valid(Layer::L2, self.l2.backend_type.clone());
}
Err(e) => {
result.add_invalid(Layer::L2, self.l2.backend_type.clone(), e.to_string());
}
}
}
result
}
pub fn validate_and_fix(&self) -> (FixedConfigResult, Option<CustomTieredConfig>) {
let validation = self.validate();
let fixes = validation.get_fixes();
if fixes.is_empty() || !self.auto_fix.enabled {
return (FixedConfigResult::from(validation), None);
}
let mut fixed = self.clone();
for fix in fixes {
match fix.layer {
Layer::L1 => {
if self.auto_fix.warn_on_fix {
tracing::warn!(
"L1 backend '{}' is not suitable for L1, auto-fixing to '{}'",
fix.from_backend,
fix.to_backend
);
}
fixed.l1.backend_type = fix.to_backend.clone();
}
Layer::L2 => {
if self.auto_fix.warn_on_fix {
tracing::warn!(
"L2 backend '{}' is not suitable for L2, auto-fixing to '{}'",
fix.from_backend,
fix.to_backend
);
}
fixed.l2.backend_type = fix.to_backend.clone();
}
}
}
let fixed_validation = fixed.validate();
let fixed_result = FixedConfigResult::from(fixed_validation);
if fixed_result.is_valid() {
(fixed_result, Some(fixed))
} else {
tracing::error!("Auto-fix failed for tiered cache configuration");
(FixedConfigResult::from(validation), None)
}
}
}
#[derive(Debug, Clone, Default)]
pub struct ConfigValidationResult {
valid_layers: Vec<(Layer, BackendType)>,
invalid_layers: Vec<(Layer, BackendType, String)>,
fixes: Vec<ConfigFix>,
}
impl ConfigValidationResult {
pub fn new() -> Self {
Self {
valid_layers: Vec::new(),
invalid_layers: Vec::new(),
fixes: Vec::new(),
}
}
fn add_valid(&mut self, layer: Layer, backend_type: BackendType) {
self.valid_layers.push((layer, backend_type));
}
fn add_invalid(&mut self, layer: Layer, backend_type: BackendType, error: String) {
let backend_type_clone = backend_type.clone();
self.invalid_layers
.push((layer, backend_type, error.clone()));
let suggested = backend_type_clone.recommended_layer();
if suggested != layer {
let recommended = match layer {
Layer::L1 => BackendType::default(),
Layer::L2 => {
#[cfg(feature = "l2-redis")]
{
BackendType::Redis
}
#[cfg(not(feature = "l2-redis"))]
{
BackendType::Memory
}
}
};
self.fixes.push(ConfigFix {
layer,
from_backend: backend_type_clone,
to_backend: recommended,
reason: error,
});
}
}
pub fn is_valid(&self) -> bool {
self.invalid_layers.is_empty()
}
pub fn has_warnings(&self) -> bool {
!self.fixes.is_empty()
}
pub fn get_fixes(&self) -> &[ConfigFix] {
&self.fixes
}
pub fn get_validation_report(&self) -> String {
let mut report = String::new();
if self.is_valid() {
report.push_str("✅ Configuration is valid\n");
} else {
report.push_str("❌ Configuration has issues:\n");
for (layer, backend, error) in &self.invalid_layers {
report.push_str(&format!(" - Layer {}: {} - {}\n", layer, backend, error));
}
}
if !self.fixes.is_empty() {
report.push_str("\n🔧 Suggested fixes:\n");
for fix in &self.fixes {
report.push_str(&format!(
" - {}: '{}' → '{}' (reason: {})\n",
fix.layer, fix.from_backend, fix.to_backend, fix.reason
));
}
}
report
}
}
#[derive(Debug, Clone)]
pub struct ConfigFix {
pub layer: Layer,
pub from_backend: BackendType,
pub to_backend: BackendType,
pub reason: String,
}
#[derive(Debug, Clone)]
pub struct FixedConfigResult {
pub is_valid: bool,
pub l1_backend: Option<BackendType>,
pub l2_backend: Option<BackendType>,
pub warnings: Vec<String>,
}
impl From<ConfigValidationResult> for FixedConfigResult {
fn from(val: ConfigValidationResult) -> Self {
let mut warnings = Vec::new();
for fix in &val.fixes {
warnings.push(format!(
"Auto-fixed {} from '{}' to '{}'",
fix.layer, fix.from_backend, fix.to_backend
));
}
let l1_backend = val
.valid_layers
.iter()
.find(|(l, _)| *l == Layer::L1)
.map(|(_, b)| b.clone());
let l2_backend = val
.valid_layers
.iter()
.find(|(l, _)| *l == Layer::L2)
.map(|(_, b)| b.clone());
Self {
is_valid: val.is_valid(),
l1_backend,
l2_backend,
warnings,
}
}
}
impl FixedConfigResult {
pub fn is_valid(&self) -> bool {
self.is_valid
}
pub fn get_report(&self) -> String {
let mut report = String::new();
if !self.is_valid {
report.push_str("Invalid configuration:\n");
}
for warning in &self.warnings {
report.push_str(&format!(" - {}\n", warning));
}
report
}
}
pub struct CustomTieredConfigBuilder(CustomTieredConfig);
impl Default for CustomTieredConfigBuilder {
fn default() -> Self {
Self(CustomTieredConfig::new())
}
}
impl CustomTieredConfigBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn l1(mut self, backend_type: BackendType) -> Self {
self.0.l1.backend_type = backend_type;
self
}
pub fn l1_options(mut self, options: serde_json::Value) -> Self {
self.0.l1.options = options;
self
}
pub fn l2(mut self, backend_type: BackendType) -> Self {
self.0.l2.backend_type = backend_type;
self
}
pub fn l2_options(mut self, options: serde_json::Value) -> Self {
self.0.l2.options = options;
self
}
pub fn enable_l1(mut self, enabled: bool) -> Self {
self.0.l1.enabled = enabled;
self
}
pub fn enable_l2(mut self, enabled: bool) -> Self {
self.0.l2.enabled = enabled;
self
}
pub fn auto_fix(mut self, enabled: bool) -> Self {
self.0.auto_fix.enabled = enabled;
self
}
pub fn build(self) -> CustomTieredConfig {
self.0
}
}
#[cfg(feature = "confers")]
#[instrument(skip(path, validation_config), level = "debug")]
pub async fn load_from_file(
path: &str,
validation_config: Option<PathValidationConfig>,
) -> Result<CustomTieredConfig> {
use std::fs;
use toml;
let path_config = validation_config.unwrap_or_default();
let safe_path = path_config.validate(path)?;
if let Ok(metadata) = fs::metadata(&safe_path) {
if metadata.file_type().is_symlink() {
return Err(CacheError::ConfigError(
"Symbolic links are not allowed for configuration files".to_string(),
));
}
}
let content =
fs::read_to_string(&safe_path).map_err(|e| CacheError::ConfigError(e.to_string()))?;
let config: CustomTieredConfig =
toml::from_str(&content).map_err(|e| CacheError::ConfigError(e.to_string()))?;
let (result, fixed) = config.validate_and_fix();
if !result.is_valid() {
return Err(CacheError::ConfigError(format!(
"Invalid tiered cache configuration: {}",
result.get_report()
)));
}
if let Some(fixed_config) = fixed {
if !result.warnings.is_empty() {
tracing::info!(
"Auto-fixed tiered cache configuration: {:?}",
result.warnings
);
}
Ok(fixed_config)
} else {
Ok(config)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_backend_type_layer_restriction() {
#[cfg(feature = "l1-moka")]
{
assert_eq!(
BackendType::Moka.layer_restriction(),
LayerRestriction::L1Only
);
assert!(BackendType::Moka.supports_layer(Layer::L1));
assert!(!BackendType::Moka.supports_layer(Layer::L2));
}
#[cfg(feature = "l2-redis")]
{
assert_eq!(
BackendType::Redis.layer_restriction(),
LayerRestriction::L2Only
);
assert!(!BackendType::Redis.supports_layer(Layer::L1));
assert!(BackendType::Redis.supports_layer(Layer::L2));
}
assert_eq!(
BackendType::Tiered.layer_restriction(),
LayerRestriction::Any
);
assert!(BackendType::Tiered.supports_layer(Layer::L1));
assert!(BackendType::Tiered.supports_layer(Layer::L2));
}
#[test]
fn test_backend_type_recommended_layer() {
#[cfg(feature = "l1-moka")]
assert_eq!(BackendType::Moka.recommended_layer(), Layer::L1);
#[cfg(feature = "l2-redis")]
assert_eq!(BackendType::Redis.recommended_layer(), Layer::L2);
}
#[test]
fn test_custom_tiered_config_validation() {
let config = CustomTieredConfig::new();
#[cfg(feature = "l1-moka")]
{
assert_eq!(config.l1.backend_type, BackendType::Moka);
}
#[cfg(feature = "l2-redis")]
{
assert_eq!(config.l2.backend_type, BackendType::Redis);
}
}
#[test]
fn test_invalid_config_auto_fix() {
let mut config = CustomTieredConfig::new();
#[cfg(feature = "l2-redis")]
{
config.l1.backend_type = BackendType::Redis;
config.auto_fix.enabled = true;
let result = config.validate();
assert!(!result.is_valid());
assert!(result.has_warnings());
}
}
#[test]
fn test_custom_tiered_config_builder() {
let config = CustomTieredConfigBuilder::new()
.l1(BackendType::Tiered)
.l2(BackendType::Tiered)
.enable_l1(true)
.enable_l2(true)
.auto_fix(true)
.build();
assert_eq!(config.l1.backend_type, BackendType::Tiered);
assert_eq!(config.l2.backend_type, BackendType::Tiered);
assert!(config.auto_fix.enabled);
}
#[test]
fn test_layer_backend_config_validate() {
#[cfg(feature = "l1-moka")]
{
let config = LayerBackendConfig::new(BackendType::Moka);
assert!(config.validate(Layer::L1).is_ok());
assert!(config.validate(Layer::L2).is_err());
}
#[cfg(feature = "l2-redis")]
{
let config = LayerBackendConfig::new(BackendType::Redis);
assert!(config.validate(Layer::L1).is_err());
assert!(config.validate(Layer::L2).is_ok());
}
}
#[test]
fn test_config_validation_result() {
let mut result = ConfigValidationResult::new();
result.add_valid(Layer::L1, BackendType::Tiered);
result.add_valid(Layer::L2, BackendType::Tiered);
assert!(result.is_valid());
assert!(!result.has_warnings());
}
#[test]
fn test_config_validation_result_with_warnings() {
let mut result = ConfigValidationResult::new();
result.add_valid(Layer::L2, BackendType::Tiered);
#[cfg(feature = "l2-redis")]
{
result.add_invalid(
Layer::L1,
BackendType::Redis,
"Redis is not supported in L1".to_string(),
);
}
#[cfg(feature = "l2-redis")]
{
assert!(!result.is_valid());
assert!(result.has_warnings());
assert!(!result.get_fixes().is_empty());
}
}
#[test]
fn test_fixed_config_result_from_validation() {
let mut result = ConfigValidationResult::new();
result.add_valid(Layer::L1, BackendType::Tiered);
result.add_valid(Layer::L2, BackendType::Tiered);
let fixed: FixedConfigResult = result.into();
assert!(fixed.is_valid);
assert_eq!(fixed.l1_backend, Some(BackendType::Tiered));
assert_eq!(fixed.l2_backend, Some(BackendType::Tiered));
assert!(fixed.warnings.is_empty());
}
#[test]
fn test_auto_fix_config_defaults() {
let config = AutoFixConfig::default();
assert!(config.enabled);
assert!(config.warn_on_fix);
}
#[test]
fn test_auto_fix_config_builder() {
let config = AutoFixConfig::new()
.with_enabled(false)
.with_warn_on_fix(false);
assert!(!config.enabled);
assert!(!config.warn_on_fix);
}
#[test]
fn test_backend_type_available_backends() {
let backends = BackendType::available_backends();
assert!(backends.contains(&BackendType::Tiered));
#[cfg(feature = "l1-moka")]
{
assert!(backends.contains(&BackendType::Moka));
}
#[cfg(feature = "l2-redis")]
{
assert!(backends.contains(&BackendType::Redis));
}
}
#[test]
fn test_config_validation_capacity_limits() {
assert!(ConfigValidation::validate_capacity(1000).is_ok());
assert!(ConfigValidation::validate_capacity(ConfigValidation::MAX_CAPACITY).is_ok());
assert!(ConfigValidation::validate_capacity(0).is_err());
assert!(ConfigValidation::validate_capacity(ConfigValidation::MAX_CAPACITY + 1).is_err());
}
#[test]
fn test_config_validation_ttl_limits() {
assert!(ConfigValidation::validate_ttl(3600).is_ok());
assert!(ConfigValidation::validate_ttl(ConfigValidation::MAX_TTL_SECS).is_ok());
assert!(ConfigValidation::validate_ttl(0).is_err());
assert!(ConfigValidation::validate_ttl(ConfigValidation::MAX_TTL_SECS + 1).is_err());
}
#[test]
fn test_config_validation_tti_limits() {
assert!(ConfigValidation::validate_tti(1800).is_ok());
assert!(ConfigValidation::validate_tti(ConfigValidation::MAX_TTI_SECS).is_ok());
assert!(ConfigValidation::validate_tti(ConfigValidation::MAX_TTI_SECS + 1).is_err());
}
#[test]
fn test_config_validation_custom_name() {
assert!(ConfigValidation::validate_custom_name("valid_name").is_ok());
assert!(ConfigValidation::validate_custom_name("my-backend.123").is_ok());
assert!(ConfigValidation::validate_custom_name("A").is_ok());
assert!(ConfigValidation::validate_custom_name("").is_err());
let long_name = "a".repeat(ConfigValidation::MAX_CUSTOM_NAME_LENGTH + 1);
assert!(ConfigValidation::validate_custom_name(&long_name).is_err());
assert!(ConfigValidation::validate_custom_name("invalid/name").is_err());
assert!(ConfigValidation::validate_custom_name("invalid@name").is_err());
assert!(ConfigValidation::validate_custom_name("invalid name").is_err());
}
#[test]
fn test_backend_type_from_str_validates_custom_name() {
let result = BackendType::from_str("custom:valid_name");
assert!(result.is_ok());
assert_eq!(
result.unwrap(),
BackendType::Custom("valid_name".to_string())
);
let long_name = format!(
"custom:{}",
"a".repeat(ConfigValidation::MAX_CUSTOM_NAME_LENGTH + 1)
);
let result = BackendType::from_str(&long_name);
assert!(result.is_err());
let result = BackendType::from_str("custom:invalid/name");
assert!(result.is_err());
}
#[test]
fn test_path_validation_config_defaults() {
let config = PathValidationConfig::new();
assert!(config.allowed_base_dirs.is_empty());
assert!(!config.allow_symbolic_links);
assert_eq!(config.max_path_length, 4096);
}
#[test]
fn test_path_validation_rejects_relative_paths() {
let config = PathValidationConfig::new();
let result = config.validate("relative/path/config.toml");
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("absolute"));
}
#[test]
fn test_path_validation_rejects_invalid_chars() {
let config = PathValidationConfig::new();
let result = config.validate("/path/with\ninvalid/chars.toml");
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("invalid character"));
}
#[test]
fn test_path_validation_allows_valid_absolute_paths() {
let config = PathValidationConfig::new();
let temp_path = "/tmp/oxcache_test_config.toml";
let result = config.validate(temp_path);
assert!(result.is_ok() || result.unwrap_err().to_string().contains("canonicalize"));
}
}