1use anyhow::{Context, Result};
2use serde::{Deserialize, Serialize};
3use std::fs;
4use std::path::{Path, PathBuf};
5
6#[derive(Debug, Clone, Serialize, Deserialize, Default)]
8pub struct Config {
9 pub default_secret: Option<String>,
11 pub default_algorithm: Option<String>,
13 pub default_wordlist: Option<PathBuf>,
15 pub default_private_key: Option<PathBuf>,
17}
18
19impl Config {
20 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
22 let content = fs::read_to_string(path.as_ref())
23 .with_context(|| format!("Failed to read config file: {}", path.as_ref().display()))?;
24
25 toml::from_str(&content)
26 .with_context(|| format!("Failed to parse config file: {}", path.as_ref().display()))
27 }
28
29 pub fn default_config_dir() -> Option<PathBuf> {
31 if let Ok(xdg_config_home) = std::env::var("XDG_CONFIG_HOME") {
33 let path = PathBuf::from(xdg_config_home).join("jwt-hack");
34 return Some(path);
35 }
36
37 dirs::config_dir().map(|config_dir| config_dir.join("jwt-hack"))
39 }
40
41 pub fn default_config_file() -> Option<PathBuf> {
43 Self::default_config_dir().map(|dir| dir.join("config.toml"))
44 }
45
46 pub fn load(config_path: Option<&Path>) -> Result<Self> {
51 if let Some(path) = config_path {
52 return Self::from_file(path);
54 }
55
56 if let Some(default_path) = Self::default_config_file() {
58 if default_path.exists() {
59 return Self::from_file(default_path);
60 }
61 }
62
63 Ok(Self::default())
65 }
66
67 pub fn ensure_config_dir() -> Result<Option<PathBuf>> {
69 if let Some(config_dir) = Self::default_config_dir() {
70 if !config_dir.exists() {
71 fs::create_dir_all(&config_dir).with_context(|| {
72 format!(
73 "Failed to create config directory: {}",
74 config_dir.display()
75 )
76 })?;
77 }
78 Ok(Some(config_dir))
79 } else {
80 Ok(None)
81 }
82 }
83
84 pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
86 let content = toml::to_string_pretty(self).context("Failed to serialize config to TOML")?;
87
88 if let Some(parent) = path.as_ref().parent() {
90 fs::create_dir_all(parent)
91 .with_context(|| format!("Failed to create directory: {}", parent.display()))?;
92 }
93
94 fs::write(path.as_ref(), content)
95 .with_context(|| format!("Failed to write config file: {}", path.as_ref().display()))?;
96
97 Ok(())
98 }
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104 use tempfile::TempDir;
105
106 #[test]
107 fn test_default_config() {
108 let config = Config::default();
109 assert!(config.default_secret.is_none());
110 assert!(config.default_algorithm.is_none());
111 assert!(config.default_wordlist.is_none());
112 assert!(config.default_private_key.is_none());
113 }
114
115 #[test]
116 fn test_config_serialization() {
117 let config = Config {
118 default_secret: Some("test_secret".to_string()),
119 default_algorithm: Some("HS256".to_string()),
120 default_wordlist: Some(PathBuf::from("/path/to/wordlist.txt")),
121 default_private_key: Some(PathBuf::from("/path/to/key.pem")),
122 };
123
124 let toml_str = toml::to_string(&config).unwrap();
125 let deserialized: Config = toml::from_str(&toml_str).unwrap();
126
127 assert_eq!(config.default_secret, deserialized.default_secret);
128 assert_eq!(config.default_algorithm, deserialized.default_algorithm);
129 assert_eq!(config.default_wordlist, deserialized.default_wordlist);
130 assert_eq!(config.default_private_key, deserialized.default_private_key);
131 }
132
133 #[test]
134 fn test_config_from_file() {
135 let temp_dir = TempDir::new().unwrap();
136 let config_file = temp_dir.path().join("test_config.toml");
137
138 let config_content = r#"
139default_secret = "my_secret"
140default_algorithm = "HS512"
141default_wordlist = "/path/to/wordlist.txt"
142default_private_key = "/path/to/private.pem"
143"#;
144
145 fs::write(&config_file, config_content).unwrap();
146
147 let config = Config::from_file(&config_file).unwrap();
148 assert_eq!(config.default_secret, Some("my_secret".to_string()));
149 assert_eq!(config.default_algorithm, Some("HS512".to_string()));
150 assert_eq!(
151 config.default_wordlist,
152 Some(PathBuf::from("/path/to/wordlist.txt"))
153 );
154 assert_eq!(
155 config.default_private_key,
156 Some(PathBuf::from("/path/to/private.pem"))
157 );
158 }
159
160 #[test]
161 fn test_config_load_with_fallback() {
162 let config = Config::load(None).unwrap();
164 assert!(config.default_secret.is_none());
165 }
166
167 #[test]
168 fn test_save_to_file() {
169 let temp_dir = TempDir::new().unwrap();
170 let config_file = temp_dir.path().join("save_test.toml");
171
172 let config = Config {
173 default_secret: Some("saved_secret".to_string()),
174 default_algorithm: Some("HS256".to_string()),
175 default_wordlist: None,
176 default_private_key: None,
177 };
178
179 config.save_to_file(&config_file).unwrap();
180
181 let loaded_config = Config::from_file(&config_file).unwrap();
182 assert_eq!(config.default_secret, loaded_config.default_secret);
183 assert_eq!(config.default_algorithm, loaded_config.default_algorithm);
184 }
185
186 #[test]
187 fn test_default_config_dir() {
188 let _config_dir = Config::default_config_dir();
191 }
192
193 #[test]
194 fn test_ensure_config_dir() {
195 let _result = Config::ensure_config_dir();
197 }
198}