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!(
52 f,
53 "Invalid prefix (expected '{}', got '{}')",
54 expected, actual
55 )
56 }
57 Self::UnknownProvider { provider } => {
58 write!(f, "Unknown provider: {}", provider)
59 }
60 }
61 }
62}
63
64#[must_use]
87pub fn validate_key_format(provider_id: &str, key: &str) -> ValidationResult {
88 if key.is_empty() {
90 return ValidationResult::Empty;
91 }
92
93 const MIN_KEY_LENGTH: usize = 8;
95 if key.len() < MIN_KEY_LENGTH {
96 return ValidationResult::TooShort {
97 actual: key.len(),
98 minimum: MIN_KEY_LENGTH,
99 };
100 }
101
102 let Some(provider) = find_provider(provider_id) else {
104 return ValidationResult::UnknownProvider {
105 provider: provider_id.to_string(),
106 };
107 };
108
109 if let Some(expected_prefix) = provider.key_prefix {
111 if !key.starts_with(expected_prefix) {
112 let actual_prefix: String = key.chars().take(expected_prefix.len()).collect();
114 return ValidationResult::InvalidPrefix {
115 expected: expected_prefix.to_string(),
116 actual: actual_prefix,
117 };
118 }
119 }
120
121 ValidationResult::Valid
122}
123
124#[must_use]
140pub fn mask_key(key: &str) -> String {
141 const MASK: &str = "••••••••";
142 const MAX_VISIBLE: usize = 7;
143
144 if key.is_empty() {
145 return MASK.to_string();
146 }
147
148 let visible_len = key.len().min(MAX_VISIBLE);
150 let visible: String = key.chars().take(visible_len).collect();
151
152 format!("{}{}", visible, MASK)
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158
159 #[test]
160 fn test_validate_empty() {
161 assert_eq!(
162 validate_key_format("anthropic", ""),
163 ValidationResult::Empty
164 );
165 }
166
167 #[test]
168 fn test_validate_too_short() {
169 let result = validate_key_format("anthropic", "short");
170 assert!(matches!(
171 result,
172 ValidationResult::TooShort {
173 actual: 5,
174 minimum: 8
175 }
176 ));
177 }
178
179 #[test]
180 fn test_validate_invalid_prefix() {
181 let result = validate_key_format("anthropic", "sk-wrong-key-here");
182 assert!(matches!(result, ValidationResult::InvalidPrefix { .. }));
183
184 if let ValidationResult::InvalidPrefix { expected, actual } = result {
185 assert_eq!(expected, "sk-ant-");
186 assert_eq!(actual, "sk-wron");
187 }
188 }
189
190 #[test]
191 fn test_validate_valid_anthropic() {
192 let result = validate_key_format("anthropic", "sk-ant-api03-xxxxxxxxxxxxxxxx");
193 assert!(result.is_valid());
194 }
195
196 #[test]
197 fn test_validate_valid_openai() {
198 let result = validate_key_format("openai", "sk-xxxxxxxxxxxxxxxxxxxxxxxx");
199 assert!(result.is_valid());
200 }
201
202 #[test]
203 fn test_validate_valid_groq() {
204 let result = validate_key_format("groq", "gsk_xxxxxxxxxxxxxxxxxxxx");
205 assert!(result.is_valid());
206 }
207
208 #[test]
209 fn test_validate_valid_github() {
210 let result = validate_key_format("github", "ghp_xxxxxxxxxxxxxxxxxxxx");
211 assert!(result.is_valid());
212 }
213
214 #[test]
215 fn test_validate_no_prefix_required() {
216 let result = validate_key_format("mistral", "any-valid-key-format");
218 assert!(result.is_valid());
219 }
220
221 #[test]
222 fn test_validate_unknown_provider() {
223 let result = validate_key_format("unknown_provider", "some-key");
224 assert!(matches!(result, ValidationResult::UnknownProvider { .. }));
225 }
226
227 #[test]
228 fn test_mask_key() {
229 assert_eq!(mask_key("sk-ant-api03-secret"), "sk-ant-••••••••");
230 assert_eq!(mask_key("ghp_xxxxxxxxxxxx"), "ghp_xxx••••••••");
231 assert_eq!(mask_key("short"), "short••••••••");
232 assert_eq!(mask_key(""), "••••••••");
233 }
234
235 #[test]
236 fn test_mask_key_max_visible() {
237 let long_key = "1234567890123456789";
239 let masked = mask_key(long_key);
240 assert_eq!(masked, "1234567••••••••");
241 }
242}