doum_cli/system/
config.rs

1use crate::system::paths::get_config_path;
2use anyhow::{Context, Result};
3use serde::{Deserialize, Serialize};
4use std::fs;
5use std::path::PathBuf;
6
7#[cfg(unix)]
8use std::os::unix::fs::PermissionsExt;
9
10/// Entire application configuration
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct Config {
13    pub llm: LLMConfig,
14    pub context: ContextConfig,
15    pub logging: LoggingConfig,
16}
17
18/// Configuration for LLM API
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct LLMConfig {
21    pub provider: String,
22    pub model: String,
23    pub timeout: u64,
24    pub max_retries: u32,
25    pub use_thinking: bool,
26    pub use_web_search: bool,
27}
28
29/// Context management settings
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct ContextConfig {
32    pub max_lines: usize,
33    pub max_size_kb: usize,
34}
35
36/// Configuration for logging
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct LoggingConfig {
39    pub enabled: bool,
40    pub level: String,
41}
42
43/// Ensure configuration directory and return config file path
44fn ensure_config() -> Result<PathBuf> {
45    let config_path = get_config_path()?;
46
47    if let Some(parent) = config_path.parent()
48        && !parent.exists()
49    {
50        fs::create_dir_all(parent).context("Failed to create config directory")?;
51
52        // Set directory permissions to 700 on Unix
53        #[cfg(unix)]
54        {
55            let metadata = fs::metadata(parent).context("Failed to read directory metadata")?;
56            let mut permissions = metadata.permissions();
57            permissions.set_mode(0o700);
58            fs::set_permissions(parent, permissions)
59                .context("Failed to set directory permissions")?;
60        }
61    }
62
63    Ok(config_path)
64}
65
66/// load configuration from file or create default
67pub fn load_config() -> Result<Config> {
68    let config_path = ensure_config()?;
69
70    if config_path.exists() {
71        // Validate file permissions
72        validate_config(&config_path)?;
73
74        // Read file content
75        let content = fs::read_to_string(&config_path).context("Failed to read config file")?;
76
77        // Parse TOML content
78        let config: Config = toml::from_str(&content).context("Failed to parse config file")?;
79
80        Ok(config)
81    } else {
82        // If config file doesn't exist, create default
83        let config = load_default_config()?;
84        save_config(&config)?;
85        Ok(config)
86    }
87}
88
89/// Load default configuration
90pub fn load_default_config() -> Result<Config> {
91    Ok(Config {
92        llm: LLMConfig {
93            provider: "openai".to_string(),
94            model: "gpt-5".to_string(),
95            timeout: 30,
96            max_retries: 3,
97            use_thinking: false,
98            use_web_search: true,
99        },
100        context: ContextConfig {
101            max_lines: 100,
102            max_size_kb: 50,
103        },
104        logging: LoggingConfig {
105            enabled: false,
106            level: "info".to_string(),
107        },
108    })
109}
110
111/// Save configuration to file with secure permissions
112pub fn save_config(config: &Config) -> Result<()> {
113    let config_path = ensure_config()?;
114
115    // Serialize configuration to TOML
116    let content = toml::to_string_pretty(config).context("Failed to serialize config")?;
117
118    // Write to file
119    fs::write(&config_path, content).context("Failed to write config file")?;
120
121    // if Windows, set ACLs for the user only
122    #[cfg(windows)]
123    {
124        // Windows의 경우 기본 ACL이 이미 적절하게 설정되어 있음
125        // 추가 보안이 필요한 경우 winapi를 사용하여 ACL 설정 가능
126    }
127
128    // if Unix, set file permissions to 600
129    #[cfg(unix)]
130    {
131        let metadata = fs::metadata(&config_path).context("File metadata read failed")?;
132        let mut permissions = metadata.permissions();
133        permissions.set_mode(0o600);
134        fs::set_permissions(&config_path, permissions).context("Failed to set file permissions")?;
135    }
136
137    Ok(())
138}
139
140/// Validate configuration file permissions
141fn validate_config(path: &PathBuf) -> Result<()> {
142    #[cfg(windows)]
143    {
144        // Windows에서는 기본적으로 안전하다고 가정
145        // 추가 검증이 필요한 경우 구현 가능
146        let _ = path; // unused warning 방지
147    }
148
149    #[cfg(unix)]
150    {
151        let metadata = fs::metadata(path).context("Failed to read file metadata")?;
152        let permissions = metadata.permissions();
153        let mode = permissions.mode() & 0o777;
154
155        // Check if permissions are 600 or 400
156        if mode != 0o600 && mode != 0o400 {
157            anyhow::bail!(
158                "Insecure config file permissions: {:o} on {}. Expected 600 or 400.",
159                mode,
160                path.display()
161            );
162        }
163    }
164
165    Ok(())
166}