Skip to main content

csaf_core/
config.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 Pierre Gronau, ndaal in Cologne
3
4//! Application configuration loaded from TOML.
5
6use std::path::{Path, PathBuf};
7
8use serde::{Deserialize, Serialize};
9
10use crate::error::{CsafError, Result};
11
12/// Bootstrap application configuration (from TOML file).
13///
14/// Runtime-changeable settings (theme, CSAF mode, etc.) are stored in redb
15/// via the `Settings` model. This config only contains startup parameters.
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct AppConfig {
18    /// Listen address (default: `"127.0.0.1"`).
19    #[serde(default = "default_listen_addr")]
20    pub listen_addr: String,
21
22    /// Listen port (default: `8443`).
23    #[serde(default = "default_listen_port")]
24    pub listen_port: u16,
25
26    /// Data directory for databases.
27    #[serde(default = "default_data_dir")]
28    pub data_dir: PathBuf,
29
30    /// TLS certificate file path (PEM). Auto-generated if absent.
31    pub tls_cert: Option<PathBuf>,
32
33    /// TLS private key file path (PEM). Auto-generated if absent.
34    pub tls_key: Option<PathBuf>,
35}
36
37impl Default for AppConfig {
38    fn default() -> Self {
39        Self {
40            listen_addr: default_listen_addr(),
41            listen_port: default_listen_port(),
42            data_dir: default_data_dir(),
43            tls_cert: None,
44            tls_key: None,
45        }
46    }
47}
48
49impl AppConfig {
50    /// Load configuration from a TOML file, falling back to defaults.
51    ///
52    /// # Errors
53    ///
54    /// Returns an error if the file exists but cannot be read or parsed.
55    pub fn load(path: &Path) -> Result<Self> {
56        if path.exists() {
57            let content =
58                std::fs::read_to_string(path).map_err(|e| CsafError::Config(e.to_string()))?;
59            toml::from_str(&content).map_err(|e| CsafError::Config(e.to_string()))
60        } else {
61            Ok(Self::default())
62        }
63    }
64
65    /// Path to the redb database file.
66    #[must_use]
67    pub fn redb_path(&self) -> PathBuf {
68        self.data_dir.join("csaf.redb")
69    }
70
71    /// Path to the SQLite database file.
72    #[must_use]
73    pub fn sqlite_path(&self) -> PathBuf {
74        self.data_dir.join("csaf.sqlite")
75    }
76
77    /// Full listen address string (e.g. `"127.0.0.1:8443"`).
78    #[must_use]
79    pub fn listen_address(&self) -> String {
80        format!("{}:{}", self.listen_addr, self.listen_port)
81    }
82}
83
84fn default_listen_addr() -> String {
85    "127.0.0.1".to_owned()
86}
87
88const fn default_listen_port() -> u16 {
89    8443
90}
91
92fn default_data_dir() -> PathBuf {
93    PathBuf::from("./data")
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn test_default_config() {
102        let config = AppConfig::default();
103        assert_eq!(config.listen_addr, "127.0.0.1");
104        assert_eq!(config.listen_port, 8443);
105        assert_eq!(config.listen_address(), "127.0.0.1:8443");
106        assert_eq!(config.redb_path(), PathBuf::from("./data/csaf.redb"));
107        assert_eq!(config.sqlite_path(), PathBuf::from("./data/csaf.sqlite"));
108    }
109
110    #[test]
111    fn test_load_missing_file_uses_defaults() {
112        let config =
113            AppConfig::load(Path::new("/nonexistent/config.toml")).expect("should return defaults");
114        assert_eq!(config.listen_port, 8443);
115    }
116}