csaf-core 0.3.1

CSAF storage, validation, sidecar generation, import/export
Documentation
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2026 Pierre Gronau, ndaal in Cologne

//! Application configuration loaded from TOML.

use std::path::{Path, PathBuf};

use serde::{Deserialize, Serialize};

use crate::error::{CsafError, Result};

/// Bootstrap application configuration (from TOML file).
///
/// Runtime-changeable settings (theme, CSAF mode, etc.) are stored in redb
/// via the `Settings` model. This config only contains startup parameters.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppConfig {
    /// Listen address (default: `"127.0.0.1"`).
    #[serde(default = "default_listen_addr")]
    pub listen_addr: String,

    /// Listen port (default: `8443`).
    #[serde(default = "default_listen_port")]
    pub listen_port: u16,

    /// Data directory for databases.
    #[serde(default = "default_data_dir")]
    pub data_dir: PathBuf,

    /// TLS certificate file path (PEM). Auto-generated if absent.
    pub tls_cert: Option<PathBuf>,

    /// TLS private key file path (PEM). Auto-generated if absent.
    pub tls_key: Option<PathBuf>,
}

impl Default for AppConfig {
    fn default() -> Self {
        Self {
            listen_addr: default_listen_addr(),
            listen_port: default_listen_port(),
            data_dir: default_data_dir(),
            tls_cert: None,
            tls_key: None,
        }
    }
}

impl AppConfig {
    /// Load configuration from a TOML file, falling back to defaults.
    ///
    /// # Errors
    ///
    /// Returns an error if the file exists but cannot be read or parsed.
    pub fn load(path: &Path) -> Result<Self> {
        if path.exists() {
            let content =
                std::fs::read_to_string(path).map_err(|e| CsafError::Config(e.to_string()))?;
            toml::from_str(&content).map_err(|e| CsafError::Config(e.to_string()))
        } else {
            Ok(Self::default())
        }
    }

    /// Path to the redb database file.
    #[must_use]
    pub fn redb_path(&self) -> PathBuf {
        self.data_dir.join("csaf.redb")
    }

    /// Path to the SQLite database file.
    #[must_use]
    pub fn sqlite_path(&self) -> PathBuf {
        self.data_dir.join("csaf.sqlite")
    }

    /// Full listen address string (e.g. `"127.0.0.1:8443"`).
    #[must_use]
    pub fn listen_address(&self) -> String {
        format!("{}:{}", self.listen_addr, self.listen_port)
    }
}

fn default_listen_addr() -> String {
    "127.0.0.1".to_owned()
}

const fn default_listen_port() -> u16 {
    8443
}

fn default_data_dir() -> PathBuf {
    PathBuf::from("./data")
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_default_config() {
        let config = AppConfig::default();
        assert_eq!(config.listen_addr, "127.0.0.1");
        assert_eq!(config.listen_port, 8443);
        assert_eq!(config.listen_address(), "127.0.0.1:8443");
        assert_eq!(config.redb_path(), PathBuf::from("./data/csaf.redb"));
        assert_eq!(config.sqlite_path(), PathBuf::from("./data/csaf.sqlite"));
    }

    #[test]
    fn test_load_missing_file_uses_defaults() {
        let config =
            AppConfig::load(Path::new("/nonexistent/config.toml")).expect("should return defaults");
        assert_eq!(config.listen_port, 8443);
    }
}