Skip to main content

envvault/config/
settings.rs

1use std::path::{Path, PathBuf};
2
3use serde::{Deserialize, Serialize};
4
5use crate::errors::{EnvVaultError, Result};
6
7/// Project-level configuration, loaded from `.envvault.toml`.
8///
9/// Every field has a sensible default so EnvVault works out-of-the-box
10/// without any config file at all.
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct Settings {
13    /// Which environment to use when none is specified (e.g. "dev").
14    #[serde(default = "default_environment")]
15    pub default_environment: String,
16
17    /// Directory (relative to project root) where vault files are stored.
18    #[serde(default = "default_vault_dir")]
19    pub vault_dir: String,
20
21    /// Argon2 memory cost in KiB (default: 64 MB).
22    #[serde(default = "default_argon2_memory_kib")]
23    pub argon2_memory_kib: u32,
24
25    /// Argon2 iteration count (default: 3).
26    #[serde(default = "default_argon2_iterations")]
27    pub argon2_iterations: u32,
28
29    /// Argon2 parallelism degree (default: 4).
30    #[serde(default = "default_argon2_parallelism")]
31    pub argon2_parallelism: u32,
32}
33
34// ── Serde default helpers ────────────────────────────────────────────
35
36fn default_environment() -> String {
37    "dev".to_string()
38}
39
40fn default_vault_dir() -> String {
41    ".envvault".to_string()
42}
43
44fn default_argon2_memory_kib() -> u32 {
45    65_536 // 64 MB
46}
47
48fn default_argon2_iterations() -> u32 {
49    3
50}
51
52fn default_argon2_parallelism() -> u32 {
53    4
54}
55
56// ── Implementation ───────────────────────────────────────────────────
57
58impl Default for Settings {
59    fn default() -> Self {
60        Self {
61            default_environment: default_environment(),
62            vault_dir: default_vault_dir(),
63            argon2_memory_kib: default_argon2_memory_kib(),
64            argon2_iterations: default_argon2_iterations(),
65            argon2_parallelism: default_argon2_parallelism(),
66        }
67    }
68}
69
70impl Settings {
71    /// Name of the config file we look for in the project root.
72    const FILE_NAME: &'static str = ".envvault.toml";
73
74    /// Load settings from `<project_dir>/.envvault.toml`.
75    ///
76    /// If the file does not exist, sensible defaults are returned.
77    /// If the file exists but cannot be parsed, an error is returned.
78    pub fn load(project_dir: &Path) -> Result<Self> {
79        let config_path = project_dir.join(Self::FILE_NAME);
80
81        if !config_path.exists() {
82            return Ok(Self::default());
83        }
84
85        let contents = std::fs::read_to_string(&config_path)?;
86
87        let settings: Settings = toml::from_str(&contents).map_err(|e| {
88            EnvVaultError::ConfigError(format!("Failed to parse {}: {e}", config_path.display()))
89        })?;
90
91        Ok(settings)
92    }
93
94    /// Build the full path to a vault file for a given environment.
95    ///
96    /// Example: `project_dir/.envvault/dev.vault`
97    pub fn vault_path(&self, project_dir: &Path, env_name: &str) -> PathBuf {
98        project_dir
99            .join(&self.vault_dir)
100            .join(format!("{env_name}.vault"))
101    }
102
103    /// Convert the Argon2 settings into crypto-layer params.
104    pub fn argon2_params(&self) -> crate::crypto::kdf::Argon2Params {
105        crate::crypto::kdf::Argon2Params {
106            memory_kib: self.argon2_memory_kib,
107            iterations: self.argon2_iterations,
108            parallelism: self.argon2_parallelism,
109        }
110    }
111}
112
113// ── Tests ────────────────────────────────────────────────────────────
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118    use std::fs;
119    use tempfile::TempDir;
120
121    #[test]
122    fn default_settings_are_sensible() {
123        let s = Settings::default();
124        assert_eq!(s.default_environment, "dev");
125        assert_eq!(s.vault_dir, ".envvault");
126        assert_eq!(s.argon2_memory_kib, 65_536);
127        assert_eq!(s.argon2_iterations, 3);
128        assert_eq!(s.argon2_parallelism, 4);
129    }
130
131    #[test]
132    fn load_returns_defaults_when_no_config_file() {
133        let tmp = TempDir::new().unwrap();
134        let settings = Settings::load(tmp.path()).unwrap();
135        assert_eq!(settings.default_environment, "dev");
136    }
137
138    #[test]
139    fn load_parses_toml_file() {
140        let tmp = TempDir::new().unwrap();
141        let config = r#"
142default_environment = "staging"
143vault_dir = "secrets"
144argon2_memory_kib = 131072
145argon2_iterations = 5
146argon2_parallelism = 8
147"#;
148        fs::write(tmp.path().join(".envvault.toml"), config).unwrap();
149
150        let settings = Settings::load(tmp.path()).unwrap();
151        assert_eq!(settings.default_environment, "staging");
152        assert_eq!(settings.vault_dir, "secrets");
153        assert_eq!(settings.argon2_memory_kib, 131_072);
154        assert_eq!(settings.argon2_iterations, 5);
155        assert_eq!(settings.argon2_parallelism, 8);
156    }
157
158    #[test]
159    fn load_uses_defaults_for_missing_fields() {
160        let tmp = TempDir::new().unwrap();
161        let config = "default_environment = \"prod\"\n";
162        fs::write(tmp.path().join(".envvault.toml"), config).unwrap();
163
164        let settings = Settings::load(tmp.path()).unwrap();
165        assert_eq!(settings.default_environment, "prod");
166        // Rest should be defaults
167        assert_eq!(settings.vault_dir, ".envvault");
168        assert_eq!(settings.argon2_iterations, 3);
169    }
170
171    #[test]
172    fn load_errors_on_invalid_toml() {
173        let tmp = TempDir::new().unwrap();
174        fs::write(tmp.path().join(".envvault.toml"), "not valid {{toml").unwrap();
175
176        let result = Settings::load(tmp.path());
177        assert!(result.is_err());
178    }
179
180    #[test]
181    fn vault_path_builds_correct_path() {
182        let s = Settings::default();
183        let project = Path::new("/home/user/myproject");
184        let path = s.vault_path(project, "dev");
185        assert_eq!(
186            path,
187            PathBuf::from("/home/user/myproject/.envvault/dev.vault")
188        );
189    }
190
191    #[test]
192    fn vault_path_respects_custom_vault_dir() {
193        let s = Settings {
194            vault_dir: "secrets".to_string(),
195            ..Settings::default()
196        };
197        let project = Path::new("/home/user/myproject");
198        let path = s.vault_path(project, "staging");
199        assert_eq!(
200            path,
201            PathBuf::from("/home/user/myproject/secrets/staging.vault")
202        );
203    }
204}