Skip to main content

loader/
config.rs

1//! Configuration file support for loader.
2//!
3//! Allows loading settings from TOML or JSON config files with profile support.
4
5use anyhow::{Context, Result};
6use serde::{Deserialize, Serialize};
7use std::path::Path;
8use tokio::fs;
9
10/// Configuration for a single profile
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct Profile {
13    /// Authenticated Additional Data (AAD) for AEAD
14    pub aad: Option<String>,
15
16    /// Base64-encoded 32-byte key
17    pub key_b64: Option<String>,
18
19    /// Direct URL
20    pub url: Option<String>,
21
22    /// Reversed URL (runtime reconstructed)
23    pub url_rev: Option<String>,
24
25    /// Base64-encoded URL
26    pub url_b64: Option<String>,
27
28    /// Optional output path for plaintext
29    pub output: Option<String>,
30}
31
32impl Default for Profile {
33    fn default() -> Self {
34        Self {
35            aad: None,
36            key_b64: None,
37            url: None,
38            url_rev: None,
39            url_b64: None,
40            output: None,
41        }
42    }
43}
44
45/// Root configuration structure
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct Config {
48    /// Default profile
49    pub default: Option<Profile>,
50
51    /// Named profiles for different environments
52    #[serde(default)]
53    pub profile: std::collections::HashMap<String, Profile>,
54}
55
56impl Config {
57    /// Load configuration from a TOML file
58    pub async fn from_toml_file<P: AsRef<Path>>(path: P) -> Result<Self> {
59        let content = fs::read_to_string(path.as_ref())
60            .await
61            .context("failed to read config file")?;
62        toml::from_str(&content).context("failed to parse TOML config")
63    }
64
65    /// Load configuration from a JSON file
66    pub async fn from_json_file<P: AsRef<Path>>(path: P) -> Result<Self> {
67        let content = fs::read_to_string(path.as_ref())
68            .await
69            .context("failed to read config file")?;
70        serde_json::from_str(&content).context("failed to parse JSON config")
71    }
72
73    /// Get a profile by name, falling back to default
74    pub fn get_profile(&self, name: Option<&str>) -> Option<Profile> {
75        match name {
76            Some(profile_name) => self.profile.get(profile_name).cloned(),
77            None => self.default.clone(),
78        }
79    }
80
81    /// Merge CLI args into a profile (CLI args take precedence)
82    pub fn merge_with_cli(
83        mut profile: Profile,
84        key_b64: Option<String>,
85        url: Option<String>,
86        url_rev: Option<String>,
87        url_b64: Option<String>,
88        aad: Option<String>,
89        output: Option<String>,
90    ) -> Profile {
91        if let Some(k) = key_b64 {
92            profile.key_b64 = Some(k);
93        }
94        if let Some(u) = url {
95            profile.url = Some(u);
96        }
97        if let Some(ur) = url_rev {
98            profile.url_rev = Some(ur);
99        }
100        if let Some(ub) = url_b64 {
101            profile.url_b64 = Some(ub);
102        }
103        if let Some(a) = aad {
104            profile.aad = Some(a);
105        }
106        if let Some(o) = output {
107            profile.output = Some(o);
108        }
109        profile
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn test_profile_merge() {
119        let profile = Profile {
120            aad: Some("aad-1".to_string()),
121            key_b64: Some("key1".to_string()),
122            url: Some("https://example.com".to_string()),
123            url_rev: None,
124            url_b64: None,
125            output: None,
126        };
127
128        let merged = Config::merge_with_cli(
129            profile,
130            None,
131            Some("https://override.com".to_string()),
132            None,
133            None,
134            None,
135            None,
136        );
137
138        assert_eq!(merged.url.unwrap(), "https://override.com");
139        assert_eq!(merged.aad.unwrap(), "aad-1");
140    }
141}