jwt_hack/
config.rs

1use anyhow::{Context, Result};
2use serde::{Deserialize, Serialize};
3use std::fs;
4use std::path::{Path, PathBuf};
5
6/// Configuration structure for jwt-hack
7#[derive(Debug, Clone, Serialize, Deserialize, Default)]
8pub struct Config {
9    /// Default secret key for HMAC algorithms
10    pub default_secret: Option<String>,
11    /// Default algorithm to use
12    pub default_algorithm: Option<String>,
13    /// Default wordlist path for cracking
14    pub default_wordlist: Option<PathBuf>,
15    /// Default private key path
16    pub default_private_key: Option<PathBuf>,
17}
18
19impl Config {
20    /// Load configuration from a specific file path
21    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    /// Get the default config directory path using XDG specification
30    pub fn default_config_dir() -> Option<PathBuf> {
31        // Check XDG_CONFIG_HOME environment variable first
32        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        // Fall back to platform-specific config directory
38        dirs::config_dir().map(|config_dir| config_dir.join("jwt-hack"))
39    }
40
41    /// Get the default config file path
42    pub fn default_config_file() -> Option<PathBuf> {
43        Self::default_config_dir().map(|dir| dir.join("config.toml"))
44    }
45
46    /// Load configuration with fallback logic
47    /// 1. Use provided config file path if given
48    /// 2. Try default config file location
49    /// 3. Return default config if no file exists
50    pub fn load(config_path: Option<&Path>) -> Result<Self> {
51        if let Some(path) = config_path {
52            // Use explicitly provided config file
53            return Self::from_file(path);
54        }
55
56        // Try default config file location
57        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        // Return default config if no file exists
64        Ok(Self::default())
65    }
66
67    /// Create default config directory if it doesn't exist
68    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    /// Save configuration to a file
85    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        // Ensure parent directory exists
89        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        // Test loading with non-existent file should return default config
163        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        // This test just ensures the function doesn't panic
189        // The actual path depends on the environment
190        let _config_dir = Config::default_config_dir();
191    }
192
193    #[test]
194    fn test_ensure_config_dir() {
195        // This test just ensures the function doesn't panic
196        let _result = Config::ensure_config_dir();
197    }
198}