use anyhow::{Context, Result};
use std::fs::File;
use std::io::Write;
use std::path::Path;
const DEFAULT_FORMAT: &str = "{timestamp} [{level}] {target} - {message}";
fn validate_output_path_safety(path: &Path) -> Result<()> {
let path_str = path.to_string_lossy();
let suspicious = ['\0', '\u{2024}', '\u{2025}', '\u{FE52}'];
for c in path_str.chars() {
if suspicious.contains(&c) {
let mut args = fluent_bundle::FluentArgs::new();
args.set("path", path.display().to_string());
return Err(anyhow::anyhow!(
"{}",
inklog::i18n::tr_args("cli-generate-err-path-traversal", args)
));
}
}
for component in path.components() {
if matches!(component, std::path::Component::ParentDir) {
let mut args = fluent_bundle::FluentArgs::new();
args.set("path", path.display().to_string());
return Err(anyhow::anyhow!(
"{}",
inklog::i18n::tr_args("cli-generate-err-path-traversal", args)
));
}
}
Ok(())
}
fn default_global_section() -> String {
format!(
"[global]\nlevel = \"info\"\nformat = \"{}\"",
DEFAULT_FORMAT
)
}
fn default_console_section() -> &'static str {
"[console]\nenabled = true\ncolored = true"
}
pub fn generate_config(output_path: &Path, config_type: &str) -> Result<()> {
validate_output_path_safety(output_path)?;
let output_file = if output_path.is_dir() {
output_path.join("inklog_config.toml")
} else {
output_path.to_path_buf()
};
let config_content = match config_type {
"minimal" => generate_minimal_config(),
"full" => generate_full_config(),
"database" => generate_database_config(),
"file" => generate_file_config(),
_ => {
let mut args = fluent_bundle::FluentArgs::new();
args.set("type", config_type.to_string());
return Err(anyhow::anyhow!(
"{}",
inklog::i18n::tr_args("cli-generate-unknown-type", args)
));
}
};
let mut file = File::create(&output_file).with_context(|| {
let mut args = fluent_bundle::FluentArgs::new();
args.set("path", output_file.display().to_string());
inklog::i18n::tr_args("config-create_config_failed", args)
})?;
file.write_all(config_content.as_bytes())
.with_context(|| inklog::i18n::tr("config-write_config_failed"))?;
let mut args = fluent_bundle::FluentArgs::new();
args.set("path", output_file.display().to_string());
println!("{}", inklog::i18n::tr_args("cli-generate-config", args));
Ok(())
}
fn generate_minimal_config() -> String {
format!(
r#"# inklog minimal configuration
{}
{}
"#,
default_global_section(),
default_console_section(),
)
}
fn generate_full_config() -> String {
format!(
r#"# inklog configuration
# Generated by inklog-cli generate full
{}
{}
stderr_levels = ["error", "warn"]
[file]
enabled = true
path = "logs/app.log"
max_size = "100MB"
rotation_time = "daily"
keep_files = 30
compress = true
compression_level = 3
encrypt = false
encryption_key_env = "INKLOG_ENCRYPTION_KEY"
retention_days = 30
max_total_size = "1GB"
cleanup_interval_minutes = 60
[performance]
channel_capacity = 10000
worker_threads = 3
[http]
enabled = false
host = "127.0.0.1"
port = 9090
metrics_path = "/metrics"
health_path = "/health"
# Database sink (optional)
# [database]
# enabled = false
# driver = "postgres"
# url = "postgres://localhost/logs"
# pool_size = 10
# batch_size = 100
# flush_interval_ms = 500
# table_name = "logs"
"#,
default_global_section(),
default_console_section(),
)
}
fn generate_database_config() -> String {
format!(
r#"# inklog database configuration
# Generated by inklog-cli generate database
{}
{}
[performance]
channel_capacity = 10000
worker_threads = 4
[database]
enabled = true
driver = "postgres"
url = "postgres://localhost/logs"
pool_size = 10
batch_size = 100
flush_interval_ms = 500
table_name = "logs"
# For MySQL:
# driver = "mysql"
# url = "mysql://user:password@localhost/logs"
# For SQLite:
# driver = "sqlite"
# url = "sqlite://logs.db"
# pool_size = 5
"#,
default_global_section(),
default_console_section(),
)
}
fn generate_file_config() -> String {
format!(
r#"# inklog file configuration
# Generated by inklog-cli generate file
{}
{}
[file]
enabled = true
path = "logs/app.log"
max_size = "100MB"
rotation_time = "daily"
keep_files = 30
compress = true
compression_level = 3
encrypt = false
encryption_key_env = "INKLOG_ENCRYPTION_KEY"
retention_days = 30
max_total_size = "1GB"
cleanup_interval_minutes = 60
[performance]
channel_capacity = 10000
worker_threads = 2
"#,
default_global_section(),
default_console_section(),
)
}
pub fn generate_env_example(output_path: &Path) -> Result<()> {
validate_output_path_safety(output_path)?;
let env_content = r#"# inklog environment variables example
# Copy this file to .env and customize values
# Global settings
INKLOG_LEVEL=info
INKLOG_FORMAT={timestamp} [{level}] {target} - {message}
# Console sink
INKLOG_CONSOLE_ENABLED=true
# File sink
INKLOG_FILE_ENABLED=true
INKLOG_FILE_PATH=logs/app.log
INKLOG_FILE_MAX_SIZE=100MB
INKLOG_FILE_ROTATION_TIME=daily
INKLOG_FILE_KEEP_FILES=30
INKLOG_FILE_COMPRESS=true
INKLOG_FILE_ENCRYPT=false
INKLOG_FILE_ENCRYPTION_KEY=your-encryption-key-here
# Database sink
INKLOG_DB_ENABLED=false
INKLOG_DB_DRIVER=postgres
INKLOG_DB_URL=postgres://localhost/logs
INKLOG_DB_POOL_SIZE=10
INKLOG_DB_TABLE_NAME=logs
INKLOG_DB_BATCH_SIZE=100
INKLOG_DB_FLUSH_INTERVAL_MS=500
# Performance
INKLOG_CHANNEL_CAPACITY=10000
INKLOG_WORKER_THREADS=3
# HTTP server
INKLOG_HTTP_ENABLED=false
INKLOG_HTTP_PORT=9090
# Decryption
INKLOG_DECRYPT_KEY=your-decryption-key-here
"#;
let output_file = if output_path.is_dir() {
output_path.join(".env.example")
} else {
output_path.to_path_buf()
};
let mut file = File::create(&output_file).with_context(|| {
let mut args = fluent_bundle::FluentArgs::new();
args.set("path", output_file.display().to_string());
inklog::i18n::tr_args("config-create_env_failed", args)
})?;
file.write_all(env_content.as_bytes())
.with_context(|| inklog::i18n::tr("config-write_env_failed"))?;
let mut args = fluent_bundle::FluentArgs::new();
args.set("path", output_file.display().to_string());
println!("{}", inklog::i18n::tr_args("cli-generate-env", args));
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn test_generate_config_minimal() {
let dir = tempdir().unwrap();
let output_path = dir.path().join("config.toml");
let result = generate_config(&output_path, "minimal");
assert!(result.is_ok());
let content = std::fs::read_to_string(&output_path).unwrap();
assert!(content.contains("inklog minimal configuration"));
assert!(content.contains("[global]"));
}
#[test]
fn test_generate_config_to_directory() {
let dir = tempdir().unwrap();
let result = generate_config(dir.path(), "file");
assert!(result.is_ok());
let expected = dir.path().join("inklog_config.toml");
let content = std::fs::read_to_string(&expected).unwrap();
assert!(content.contains("inklog file configuration"));
}
#[test]
fn test_generate_config_unknown_type() {
let dir = tempdir().unwrap();
let output_path = dir.path().join("config.toml");
let result = generate_config(&output_path, "unknown");
assert!(result.is_err());
let err = result.err().unwrap().to_string();
assert!(err.contains("Unknown config type"));
}
#[test]
fn test_generate_env_example() {
let dir = tempdir().unwrap();
let output_path = dir.path().join(".env.example");
let result = generate_env_example(&output_path);
assert!(result.is_ok());
let content = std::fs::read_to_string(&output_path).unwrap();
assert!(content.contains("INKLOG_LEVEL"));
assert!(content.contains("INKLOG_DECRYPT_KEY"));
}
#[test]
fn test_generate_env_example_to_directory() {
let dir = tempdir().unwrap();
let result = generate_env_example(dir.path());
assert!(result.is_ok());
let expected = dir.path().join(".env.example");
let content = std::fs::read_to_string(&expected).unwrap();
assert!(content.contains("inklog environment variables"));
}
#[test]
fn test_generate_config_rejects_path_traversal() {
let result = generate_config(Path::new("../etc/config.toml"), "minimal");
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("traversal"));
}
#[test]
fn test_generate_env_example_rejects_path_traversal() {
let result = generate_env_example(Path::new("../../etc/.env.example"));
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("traversal"));
}
#[test]
fn test_validate_output_path_safety_rejects_null_bytes() {
let result = validate_output_path_safety(Path::new("file\0.toml"));
assert!(result.is_err());
}
#[test]
fn test_validate_output_path_safety_accepts_normal_paths() {
assert!(validate_output_path_safety(Path::new("config.toml")).is_ok());
assert!(validate_output_path_safety(Path::new("/tmp/config.toml")).is_ok());
assert!(validate_output_path_safety(Path::new("subdir/config.toml")).is_ok());
}
}