use std::collections::HashMap;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::{Duration, SystemTime};
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
use toml::Value as TomlValue;
use crate::ProcessingError;
use crate::Result;
const MAX_CONFIG_SIZE: usize = 1024 * 1024;
const DEFAULT_RELOAD_INTERVAL: Duration = Duration::from_secs(30);
const BLOCKED_ENV_VARS: &[&str] = &["PATH", "HOME", "USER", "SHELL"];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Profile {
Development,
Staging,
Production,
Custom,
}
impl Default for Profile {
fn default() -> Self {
Profile::Development
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Config {
#[serde(default = "default_content_dir")]
pub content_dir: PathBuf,
#[serde(default = "default_output_dir")]
pub output_dir: PathBuf,
#[serde(default = "default_template_dir")]
pub template_dir: PathBuf,
#[serde(default)]
pub profile: Profile,
#[serde(default)]
pub content: ContentConfig,
#[serde(default)]
pub template: TemplateConfig,
#[serde(default)]
pub output: OutputConfig,
#[serde(default)]
pub custom: HashMap<String, TomlValue>,
#[serde(skip)]
last_modified: Option<SystemTime>,
#[serde(skip)]
auto_reload: bool,
#[serde(skip)]
reload_interval: Duration,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContentConfig {
#[serde(default = "default_true")]
pub validate: bool,
#[serde(default = "default_true")]
pub sanitize: bool,
#[serde(default = "default_true")]
pub extract_metadata: bool,
#[serde(default = "default_extensions")]
pub extensions: Vec<String>,
#[serde(default)]
pub options: HashMap<String, TomlValue>,
#[serde(default = "default_max_content_size")]
pub max_content_size: usize,
#[serde(default = "default_max_metadata_size")]
pub max_metadata_size: usize,
#[serde(default = "default_allowed_html_tags")]
pub allowed_html_tags: Vec<String>,
}
impl Default for ContentConfig {
fn default() -> Self {
Self {
validate: true,
sanitize: true,
extract_metadata: true,
extensions: default_extensions(),
options: HashMap::new(),
max_content_size: default_max_content_size(),
max_metadata_size: default_max_metadata_size(),
allowed_html_tags: default_allowed_html_tags(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TemplateConfig {
#[serde(default)]
pub strict_mode: bool,
#[serde(default = "default_true")]
pub cache_templates: bool,
#[serde(default)]
pub options: HashMap<String, TomlValue>,
#[serde(default = "default_max_template_size")]
pub max_template_size: usize,
#[serde(default = "default_max_cache_size")]
pub max_cache_size: usize,
#[serde(default = "default_cache_ttl")]
pub cache_ttl: u64,
#[serde(default = "default_allowed_functions")]
pub allowed_functions: Vec<String>,
}
impl Default for TemplateConfig {
fn default() -> Self {
Self {
strict_mode: false,
cache_templates: true,
options: HashMap::new(),
max_template_size: default_max_template_size(),
max_cache_size: default_max_cache_size(),
cache_ttl: default_cache_ttl(),
allowed_functions: default_allowed_functions(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OutputConfig {
#[serde(default)]
pub minify: bool,
#[serde(default = "default_true")]
pub pretty_print: bool,
#[serde(default)]
pub asset_dir: Option<PathBuf>,
#[serde(default)]
pub options: HashMap<String, TomlValue>,
#[serde(default = "default_max_output_size")]
pub max_output_size: usize,
#[cfg(unix)]
#[serde(default = "default_file_permissions")]
pub file_permissions: u32,
#[serde(default = "default_max_concurrent_ops")]
pub max_concurrent_ops: usize,
#[serde(default)]
pub rate_limit: u64,
}
impl Default for OutputConfig {
fn default() -> Self {
Self {
minify: false,
pretty_print: true,
asset_dir: None,
options: HashMap::new(),
max_output_size: default_max_output_size(),
#[cfg(unix)]
file_permissions: default_file_permissions(),
max_concurrent_ops: default_max_concurrent_ops(),
rate_limit: 0,
}
}
}
#[derive(Debug)]
pub struct ConfigBuilder {
config_file: Option<PathBuf>,
env_prefix: Option<String>,
profile: Option<Profile>,
overrides: HashMap<String, TomlValue>,
auto_reload: bool,
reload_interval: Duration,
max_file_size: usize,
}
fn default_max_content_size() -> usize {
10 * 1024 * 1024 }
fn default_max_metadata_size() -> usize {
64 * 1024 }
fn default_max_template_size() -> usize {
1024 * 1024 }
fn default_max_cache_size() -> usize {
100 * 1024 * 1024 }
fn default_max_output_size() -> usize {
100 * 1024 * 1024 }
fn default_cache_ttl() -> u64 {
3600 }
fn default_max_concurrent_ops() -> usize {
10
}
#[cfg(unix)]
fn default_file_permissions() -> u32 {
0o644
}
fn default_true() -> bool {
true
}
fn default_allowed_html_tags() -> Vec<String> {
vec![
"p",
"br",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"strong",
"em",
"del",
"ul",
"ol",
"li",
"code",
"pre",
"blockquote",
"hr",
"table",
"thead",
"tbody",
"tr",
"th",
"td",
"img",
"a",
]
.into_iter()
.map(String::from)
.collect()
}
fn default_allowed_functions() -> Vec<String> {
vec![
"upper", "lower", "trim", "date", "length", "join", "split",
"slice", "first", "last",
]
.into_iter()
.map(String::from)
.collect()
}
fn default_extensions() -> Vec<String> {
vec!["md".to_string(), "markdown".to_string()]
}
impl ConfigBuilder {
pub fn new() -> Self {
Self {
config_file: None,
env_prefix: None,
profile: None,
overrides: HashMap::new(),
auto_reload: false,
reload_interval: DEFAULT_RELOAD_INTERVAL,
max_file_size: MAX_CONFIG_SIZE,
}
}
pub fn with_file<P: AsRef<Path>>(mut self, path: P) -> Self {
self.config_file = Some(sanitize_path(path.as_ref()));
self
}
pub fn with_env_prefix<S: Into<String>>(
mut self,
prefix: S,
) -> Self {
let prefix = prefix.into();
if is_safe_env_prefix(&prefix) {
self.env_prefix = Some(prefix);
}
self
}
pub fn with_profile<P: Into<Profile>>(
mut self,
profile: P,
) -> Self {
self.profile = Some(profile.into());
self
}
pub fn with_override<K, V>(mut self, key: K, value: V) -> Self
where
K: Into<String>,
V: Into<TomlValue>,
{
let key = key.into();
let value = value.into();
if is_safe_config_key(&key) && is_safe_config_value(&value) {
_ = self.overrides.insert(key, value);
}
self
}
pub fn with_auto_reload(mut self, enabled: bool) -> Self {
self.auto_reload = enabled;
self
}
pub fn with_reload_interval(mut self, interval: Duration) -> Self {
self.reload_interval = interval.max(Duration::from_secs(1));
self
}
pub fn with_max_file_size(mut self, size: usize) -> Self {
self.max_file_size = size.min(MAX_CONFIG_SIZE);
self
}
pub fn build(self) -> Result<Arc<RwLock<Config>>> {
let mut config = if let Some(path) = self.config_file {
load_from_file(&path, self.max_file_size)?
} else {
Config::default()
};
config.auto_reload = self.auto_reload;
config.reload_interval = self.reload_interval;
if let Some(profile) = self.profile {
config.profile = profile;
}
if let Some(prefix) = self.env_prefix {
apply_env_overrides(&mut config, &prefix)?;
}
apply_overrides(&mut config, &self.overrides)?;
validate_config(&config)?;
Ok(Arc::new(RwLock::new(config)))
}
}
impl Default for ConfigBuilder {
fn default() -> Self {
Self::new()
}
}
impl Config {
pub fn validate(&self) -> Result<()> {
validate_config(self)
}
pub fn get_custom<T: serde::de::DeserializeOwned>(
&self,
key: &str,
) -> Result<Option<T>> {
if !is_safe_config_key(key) {
return Ok(None);
}
self.custom
.get(key)
.map(|v| {
toml::Value::try_into(v.clone()).map_err(|e| {
ProcessingError::Configuration {
details: format!(
"Invalid custom config value: {}",
e
),
path: None,
source: None,
}
})
})
.transpose()
}
pub fn set_custom<T: Serialize>(
&mut self,
key: &str,
value: T,
) -> Result<()> {
if !is_safe_config_key(key) {
return Err(ProcessingError::Configuration {
details: "Invalid configuration key".to_string(),
path: None,
source: None,
});
}
let value = toml::Value::try_from(value).map_err(|e| {
ProcessingError::Configuration {
details: format!("Invalid custom config value: {}", e),
path: None,
source: None,
}
})?;
if is_safe_config_value(&value) {
_ = self.custom.insert(key.to_string(), value);
Ok(())
} else {
Err(ProcessingError::Configuration {
details: "Invalid configuration value".to_string(),
path: None,
source: None,
})
}
}
pub fn needs_reload(&self) -> bool {
if !self.auto_reload {
return false;
}
if let Some(last_modified) = self.last_modified {
if let Ok(metadata) = fs::metadata("config.toml") {
if let Ok(modified) = metadata.modified() {
return modified > last_modified;
}
}
}
false
}
pub fn reload_if_needed(&mut self) -> Result<bool> {
if self.needs_reload() {
let new_config = load_from_file(
Path::new("config.toml"),
MAX_CONFIG_SIZE,
)?;
*self = new_config;
Ok(true)
} else {
Ok(false)
}
}
}
fn sanitize_path(path: &Path) -> PathBuf {
let mut components = Vec::new();
for component in path.components() {
match component {
std::path::Component::Normal(c) => components.push(c),
std::path::Component::ParentDir => {
if !components.is_empty() {
_ = components.pop();
}
}
std::path::Component::CurDir => {}
_ => {}
}
}
components.iter().collect()
}
fn is_safe_config_key(key: &str) -> bool {
let valid_chars = |c: char| {
c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.'
};
!key.is_empty()
&& key.len() <= 64
&& key.chars().all(valid_chars)
&& !key.starts_with('.')
&& !key.ends_with('.')
}
fn is_safe_config_value(value: &TomlValue) -> bool {
match value {
TomlValue::String(s) => {
s.len() <= 1024 && !contains_dangerous_patterns(s)
}
TomlValue::Array(arr) => {
arr.len() <= 100 && arr.iter().all(is_safe_config_value)
}
TomlValue::Table(table) => {
table.len() <= 50
&& table.keys().all(|k| is_safe_config_key(k.as_str()))
&& table.values().all(is_safe_config_value)
}
_ => true,
}
}
fn contains_dangerous_patterns(s: &str) -> bool {
let patterns = [
"javascript:",
"data:",
"vbscript:",
"file:",
"<script",
"eval(",
"setTimeout",
"setInterval",
];
patterns.iter().any(|p| s.to_lowercase().contains(p))
}
fn is_safe_env_prefix(prefix: &str) -> bool {
let valid_chars = |c: char| c.is_ascii_uppercase() || c == '_';
!prefix.is_empty()
&& prefix.len() <= 32
&& prefix.chars().all(valid_chars)
&& prefix.ends_with('_')
}
fn load_from_file(path: &Path, max_size: usize) -> Result<Config> {
let metadata = fs::metadata(path).map_err(|e| {
ProcessingError::Configuration {
details: format!(
"Failed to read config file metadata: {}",
e
),
path: Some(path.to_path_buf()),
source: None,
}
})?;
if metadata.len() > max_size as u64 {
return Err(ProcessingError::Configuration {
details: format!(
"Config file exceeds maximum size of {} bytes",
max_size
),
path: Some(path.to_path_buf()),
source: None,
});
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let perms = metadata.permissions();
if perms.mode() & 0o777 > 0o644 {
return Err(ProcessingError::Configuration {
details: "Config file permissions too permissive"
.to_string(),
path: Some(path.to_path_buf()),
source: None,
});
}
}
let content = fs::read_to_string(path).map_err(|e| {
ProcessingError::Configuration {
details: format!("Failed to read config file: {}", e),
path: Some(path.to_path_buf()),
source: None,
}
})?;
let mut config: Config = toml::from_str(&content).map_err(|e| {
ProcessingError::Configuration {
details: format!("Failed to parse config file: {}", e),
path: Some(path.to_path_buf()),
source: None,
}
})?;
config.last_modified =
Some(metadata.modified().unwrap_or_else(|_| SystemTime::now()));
Ok(config)
}
fn apply_env_overrides(
config: &mut Config,
prefix: &str,
) -> Result<()> {
for (key, value) in env::vars() {
if BLOCKED_ENV_VARS.contains(&key.as_str()) {
continue;
}
if let Some(stripped) = key.strip_prefix(prefix) {
let config_key =
stripped.trim_start_matches('_').to_lowercase();
if is_safe_config_key(&config_key) {
apply_config_value(config, &config_key, &value)?;
}
}
}
Ok(())
}
fn apply_overrides(
config: &mut Config,
overrides: &HashMap<String, TomlValue>,
) -> Result<()> {
for (key, value) in overrides {
if is_safe_config_key(key) && is_safe_config_value(value) {
apply_config_value(config, key, value)?;
}
}
Ok(())
}
fn apply_config_value<T: ToString>(
config: &mut Config,
key: &str,
value: &T,
) -> Result<()> {
let value_str = value.to_string().trim_matches('"').to_string();
match key {
"content_dir" => {
config.content_dir =
sanitize_path(&PathBuf::from(value_str));
}
"output_dir" => {
config.output_dir =
sanitize_path(&PathBuf::from(value_str));
}
"template_dir" => {
config.template_dir =
sanitize_path(&PathBuf::from(value_str));
}
"profile" => {
config.profile = match value_str.to_lowercase().as_str() {
"development" => Profile::Development,
"staging" => Profile::Staging,
"production" => Profile::Production,
_ => Profile::Custom,
};
}
_ => {
if let Some((section, key)) = key.split_once('.') {
match section {
"content" => apply_content_value(
&mut config.content,
key,
&value_str,
)?,
"template" => apply_template_value(
&mut config.template,
key,
&value_str,
)?,
"output" => apply_output_value(
&mut config.output,
key,
&value_str,
)?,
"custom" => {
if is_safe_config_key(key) {
let toml_value =
TomlValue::String(value_str);
if is_safe_config_value(&toml_value) {
_ = config.custom.insert(
key.to_string(),
toml_value,
);
}
}
}
_ => {
return Err(ProcessingError::Configuration {
details: format!(
"Unknown configuration section: {}",
section
),
path: None,
source: None,
});
}
}
} else {
return Err(ProcessingError::Configuration {
details: format!(
"Unknown configuration key: {}",
key
),
path: None,
source: None,
});
}
}
}
if matches!(config.profile, Profile::Production) {
enforce_production_security(config)?;
}
Ok(())
}
fn enforce_production_security(config: &mut Config) -> Result<()> {
config.content.sanitize = true;
config.template.strict_mode = true;
#[cfg(unix)]
{
config.output.file_permissions &= 0o644;
}
if config.output.rate_limit == 0 {
config.output.rate_limit = 1024 * 1024; }
if !config.content.sanitize || !config.template.strict_mode {
return Err(ProcessingError::Configuration {
details: "Cannot disable security features in production"
.to_string(),
path: None,
source: None,
});
}
Ok(())
}
fn validate_config(config: &Config) -> Result<()> {
validate_path(&config.content_dir, "content", true)?;
validate_path(&config.template_dir, "template", true)?;
if let Some(asset_dir) = &config.output.asset_dir {
validate_path(asset_dir, "asset", true)?;
}
if config.content.extensions.is_empty() {
return Err(ProcessingError::Configuration {
details: "No content extensions specified".to_string(),
path: None,
source: None,
});
}
if config.content.max_content_size > 100 * 1024 * 1024 {
return Err(ProcessingError::Configuration {
details: "Content size limit too large".to_string(),
path: None,
source: None,
});
}
Ok(())
}
fn validate_path(
path: &Path,
name: &str,
must_exist: bool,
) -> Result<()> {
let sanitized = sanitize_path(path);
if must_exist && !sanitized.exists() {
return Err(ProcessingError::Configuration {
details: format!(
"{} directory does not exist: {}",
name,
sanitized.display()
),
path: Some(sanitized),
source: None,
});
}
if sanitized.exists() && !sanitized.is_dir() {
return Err(ProcessingError::Configuration {
details: format!(
"{} path is not a directory: {}",
name,
sanitized.display()
),
path: Some(sanitized),
source: None,
});
}
Ok(())
}
fn apply_content_value(
config: &mut ContentConfig,
key: &str,
value: &str,
) -> Result<()> {
match key {
"validate" => {
config.validate = value.parse().map_err(|e| {
ProcessingError::Configuration {
details: format!(
"Invalid validate value '{}': {}",
value, e
),
path: None,
source: None,
}
})?;
}
"sanitize" => {
config.sanitize = value.parse().map_err(|e| {
ProcessingError::Configuration {
details: format!(
"Invalid sanitize value '{}': {}",
value, e
),
path: None,
source: None,
}
})?;
}
"extract_metadata" => {
config.extract_metadata = value.parse().map_err(|e| {
ProcessingError::Configuration {
details: format!(
"Invalid extract_metadata value '{}': {}",
value, e
),
path: None,
source: None,
}
})?;
}
"max_content_size" => {
config.max_content_size = value.parse().map_err(|e| {
ProcessingError::Configuration {
details: format!(
"Invalid max_content_size value '{}': {}",
value, e
),
path: None,
source: None,
}
})?;
}
_ => {
let toml_value = TomlValue::String(value.to_string());
if is_safe_config_value(&toml_value) {
_ = config.options.insert(key.to_string(), toml_value);
}
}
}
Ok(())
}
fn apply_template_value(
config: &mut TemplateConfig,
key: &str,
value: &str,
) -> Result<()> {
match key {
"strict_mode" => {
config.strict_mode = value.parse().map_err(|e| {
ProcessingError::Configuration {
details: format!(
"Invalid strict_mode value '{}': {}",
value, e
),
path: None,
source: None,
}
})?;
}
"cache_templates" => {
config.cache_templates = value.parse().map_err(|e| {
ProcessingError::Configuration {
details: format!(
"Invalid cache_templates value '{}': {}",
value, e
),
path: None,
source: None,
}
})?;
}
"max_template_size" => {
config.max_template_size = value.parse().map_err(|e| {
ProcessingError::Configuration {
details: format!(
"Invalid max_template_size value '{}': {}",
value, e
),
path: None,
source: None,
}
})?;
}
_ => {
let toml_value = TomlValue::String(value.to_string());
if is_safe_config_value(&toml_value) {
_ = config.options.insert(key.to_string(), toml_value);
}
}
}
Ok(())
}
fn apply_output_value(
config: &mut OutputConfig,
key: &str,
value: &str,
) -> Result<()> {
match key {
"minify" => {
config.minify = value.parse().map_err(|e| {
ProcessingError::Configuration {
details: format!(
"Invalid minify value '{}': {}",
value, e
),
path: None,
source: None,
}
})?;
}
"pretty_print" => {
config.pretty_print = value.parse().map_err(|e| {
ProcessingError::Configuration {
details: format!(
"Invalid pretty_print value '{}': {}",
value, e
),
path: None,
source: None,
}
})?;
}
"asset_dir" => {
config.asset_dir =
Some(sanitize_path(&PathBuf::from(value)));
}
"max_output_size" => {
config.max_output_size = value.parse().map_err(|e| {
ProcessingError::Configuration {
details: format!(
"Invalid max_output_size value '{}': {}",
value, e
),
path: None,
source: None,
}
})?;
}
_ => {
let toml_value = TomlValue::String(value.to_string());
if is_safe_config_value(&toml_value) {
_ = config.options.insert(key.to_string(), toml_value);
}
}
}
Ok(())
}
fn default_content_dir() -> PathBuf {
PathBuf::from("content")
}
fn default_output_dir() -> PathBuf {
PathBuf::from("public")
}
fn default_template_dir() -> PathBuf {
PathBuf::from("templates")
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_path_sanitization() {
let path = Path::new("../../../etc/passwd");
let sanitized = sanitize_path(path);
assert_eq!(sanitized, PathBuf::from("etc/passwd"));
}
#[test]
fn test_safe_config_key() {
assert!(is_safe_config_key("valid_key"));
assert!(is_safe_config_key("valid-key-123"));
assert!(!is_safe_config_key(""));
assert!(!is_safe_config_key("../invalid"));
assert!(!is_safe_config_key("invalid*key"));
}
#[test]
fn test_safe_config_value() {
assert!(is_safe_config_value(&TomlValue::String(
"safe value".into()
)));
assert!(!is_safe_config_value(&TomlValue::String(
"javascript:alert(1)".into()
)));
let mut large_table = toml::map::Map::new();
for i in 0..100 {
_ = large_table.insert(
format!("key{}", i),
TomlValue::String("value".into()),
);
}
assert!(!is_safe_config_value(&TomlValue::Table(large_table)));
}
#[test]
fn test_config_size_limit() {
let temp_dir = TempDir::new().unwrap();
let config_file = temp_dir.path().join("large_config.toml");
let large_content = "x".repeat(MAX_CONFIG_SIZE + 1);
fs::write(&config_file, large_content).unwrap();
assert!(load_from_file(&config_file, MAX_CONFIG_SIZE).is_err());
}
#[test]
fn test_config_builder_new_defaults() {
let builder = ConfigBuilder::new();
assert!(builder.config_file.is_none());
assert!(builder.env_prefix.is_none());
assert!(builder.profile.is_none());
assert!(builder.overrides.is_empty());
assert!(!builder.auto_reload);
assert_eq!(builder.reload_interval, DEFAULT_RELOAD_INTERVAL);
assert_eq!(builder.max_file_size, MAX_CONFIG_SIZE);
}
#[test]
fn test_config_builder_with_file() {
let builder =
ConfigBuilder::new().with_file(Path::new("config.toml"));
assert!(builder.config_file.is_some());
assert_eq!(
builder.config_file.unwrap(),
PathBuf::from("config.toml")
);
}
#[test]
fn test_config_builder_with_env_prefix() {
let builder = ConfigBuilder::new().with_env_prefix("NUCLEUS_");
assert!(builder.env_prefix.is_some());
assert_eq!(builder.env_prefix.unwrap(), "NUCLEUS_");
}
#[test]
fn test_config_builder_with_invalid_env_prefix() {
let builder = ConfigBuilder::new().with_env_prefix("nucleus");
assert!(builder.env_prefix.is_none());
}
#[test]
fn test_config_builder_with_profile() {
let builder =
ConfigBuilder::new().with_profile(Profile::Production);
assert!(builder.profile.is_some());
assert_eq!(builder.profile.unwrap(), Profile::Production);
}
#[test]
fn test_config_builder_with_override() {
let mut builder = ConfigBuilder::new();
builder = builder.with_override("custom_key", "custom_value");
assert!(builder.overrides.contains_key("custom_key"));
assert_eq!(
builder.overrides.get("custom_key").unwrap(),
&TomlValue::String("custom_value".into())
);
}
#[test]
fn test_config_builder_with_invalid_override_key() {
let mut builder = ConfigBuilder::new();
builder = builder.with_override("../invalid_key", "value");
assert!(!builder.overrides.contains_key("../invalid_key"));
}
#[test]
fn test_config_builder_with_auto_reload() {
let builder = ConfigBuilder::new().with_auto_reload(true);
assert!(builder.auto_reload);
let builder = ConfigBuilder::new().with_auto_reload(false);
assert!(!builder.auto_reload);
}
#[test]
fn test_config_builder_with_reload_interval() {
let builder = ConfigBuilder::new()
.with_reload_interval(Duration::from_secs(10));
assert_eq!(builder.reload_interval, Duration::from_secs(10));
let builder = ConfigBuilder::new()
.with_reload_interval(Duration::from_millis(500));
assert_eq!(builder.reload_interval, Duration::from_secs(1));
}
#[test]
fn test_config_builder_with_max_file_size() {
let builder =
ConfigBuilder::new().with_max_file_size(512 * 1024); assert_eq!(builder.max_file_size, 512 * 1024);
let builder = ConfigBuilder::new()
.with_max_file_size(MAX_CONFIG_SIZE * 2);
assert_eq!(builder.max_file_size, MAX_CONFIG_SIZE); }
#[test]
fn test_config_set_and_get_custom() {
let mut config = Config::default();
config.set_custom("custom_key", "custom_value").unwrap();
let custom_value: Option<String> =
config.get_custom("custom_key").unwrap();
assert_eq!(custom_value, Some("custom_value".to_string()));
let non_existent: Option<String> =
config.get_custom("non_existent_key").unwrap();
assert!(non_existent.is_none());
}
#[test]
fn test_config_set_custom_with_invalid_key() {
let mut config = Config::default();
let result = config.set_custom("../invalid_key", "value");
assert!(result.is_err());
}
}