1use crate::find_provider;
4use core::fmt;
5
6#[derive(Debug, Clone, PartialEq, Eq)]
8pub enum ValidationResult {
9 Valid,
11 Empty,
13 TooShort {
15 actual: usize,
17 minimum: usize,
19 },
20 InvalidPrefix {
22 expected: String,
24 actual: String,
26 },
27 UnknownProvider {
29 provider: String,
31 },
32}
33
34impl ValidationResult {
35 #[must_use]
37 pub fn is_valid(&self) -> bool {
38 matches!(self, ValidationResult::Valid)
39 }
40}
41
42impl fmt::Display for ValidationResult {
43 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44 match self {
45 Self::Valid => write!(f, "Valid"),
46 Self::Empty => write!(f, "Key is empty"),
47 Self::TooShort { actual, minimum } => {
48 write!(f, "Key too short ({} chars, minimum {})", actual, minimum)
49 }
50 Self::InvalidPrefix { expected, actual } => {
51 write!(f, "Invalid prefix (expected '{}', got '{}')", expected, actual)
52 }
53 Self::UnknownProvider { provider } => {
54 write!(f, "Unknown provider: {}", provider)
55 }
56 }
57 }
58}
59
60#[must_use]
83pub fn validate_key_format(provider_id: &str, key: &str) -> ValidationResult {
84 if key.is_empty() {
86 return ValidationResult::Empty;
87 }
88
89 const MIN_KEY_LENGTH: usize = 8;
91 if key.len() < MIN_KEY_LENGTH {
92 return ValidationResult::TooShort {
93 actual: key.len(),
94 minimum: MIN_KEY_LENGTH,
95 };
96 }
97
98 let Some(provider) = find_provider(provider_id) else {
100 return ValidationResult::UnknownProvider {
101 provider: provider_id.to_string(),
102 };
103 };
104
105 if let Some(expected_prefix) = provider.key_prefix {
107 if !key.starts_with(expected_prefix) {
108 let actual_prefix: String = key.chars().take(expected_prefix.len()).collect();
110 return ValidationResult::InvalidPrefix {
111 expected: expected_prefix.to_string(),
112 actual: actual_prefix,
113 };
114 }
115 }
116
117 ValidationResult::Valid
118}
119
120#[must_use]
136pub fn mask_key(key: &str) -> String {
137 const MASK: &str = "••••••••";
138 const MAX_VISIBLE: usize = 7;
139
140 if key.is_empty() {
141 return MASK.to_string();
142 }
143
144 let visible_len = key.len().min(MAX_VISIBLE);
146 let visible: String = key.chars().take(visible_len).collect();
147
148 format!("{}{}", visible, MASK)
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154
155 #[test]
156 fn test_validate_empty() {
157 assert_eq!(
158 validate_key_format("anthropic", ""),
159 ValidationResult::Empty
160 );
161 }
162
163 #[test]
164 fn test_validate_too_short() {
165 let result = validate_key_format("anthropic", "short");
166 assert!(matches!(result, ValidationResult::TooShort { actual: 5, minimum: 8 }));
167 }
168
169 #[test]
170 fn test_validate_invalid_prefix() {
171 let result = validate_key_format("anthropic", "sk-wrong-key-here");
172 assert!(matches!(result, ValidationResult::InvalidPrefix { .. }));
173
174 if let ValidationResult::InvalidPrefix { expected, actual } = result {
175 assert_eq!(expected, "sk-ant-");
176 assert_eq!(actual, "sk-wron");
177 }
178 }
179
180 #[test]
181 fn test_validate_valid_anthropic() {
182 let result = validate_key_format("anthropic", "sk-ant-api03-xxxxxxxxxxxxxxxx");
183 assert!(result.is_valid());
184 }
185
186 #[test]
187 fn test_validate_valid_openai() {
188 let result = validate_key_format("openai", "sk-xxxxxxxxxxxxxxxxxxxxxxxx");
189 assert!(result.is_valid());
190 }
191
192 #[test]
193 fn test_validate_valid_groq() {
194 let result = validate_key_format("groq", "gsk_xxxxxxxxxxxxxxxxxxxx");
195 assert!(result.is_valid());
196 }
197
198 #[test]
199 fn test_validate_valid_github() {
200 let result = validate_key_format("github", "ghp_xxxxxxxxxxxxxxxxxxxx");
201 assert!(result.is_valid());
202 }
203
204 #[test]
205 fn test_validate_no_prefix_required() {
206 let result = validate_key_format("mistral", "any-valid-key-format");
208 assert!(result.is_valid());
209 }
210
211 #[test]
212 fn test_validate_unknown_provider() {
213 let result = validate_key_format("unknown_provider", "some-key");
214 assert!(matches!(result, ValidationResult::UnknownProvider { .. }));
215 }
216
217 #[test]
218 fn test_mask_key() {
219 assert_eq!(mask_key("sk-ant-api03-secret"), "sk-ant-••••••••");
220 assert_eq!(mask_key("ghp_xxxxxxxxxxxx"), "ghp_xxx••••••••");
221 assert_eq!(mask_key("short"), "short••••••••");
222 assert_eq!(mask_key(""), "••••••••");
223 }
224
225 #[test]
226 fn test_mask_key_max_visible() {
227 let long_key = "1234567890123456789";
229 let masked = mask_key(long_key);
230 assert_eq!(masked, "1234567••••••••");
231 }
232}