1use crate::error::{ArchToolkitError, Result};
4use std::sync::LazyLock;
5
6static DEFAULT_VALIDATION_CONFIG: LazyLock<ValidationConfig> =
8 LazyLock::new(ValidationConfig::default);
9
10#[derive(Debug, Clone)]
21pub struct ValidationConfig {
22 pub strict_empty: bool,
24 pub max_query_length: usize,
26 pub max_package_name_length: usize,
28}
29
30impl Default for ValidationConfig {
31 fn default() -> Self {
32 Self {
33 strict_empty: true,
34 max_query_length: 256,
35 max_package_name_length: 127,
36 }
37 }
38}
39
40pub fn validate_package_name<'a>(
62 name: &'a str,
63 config: Option<&ValidationConfig>,
64) -> Result<&'a str> {
65 let config = config.unwrap_or(&DEFAULT_VALIDATION_CONFIG);
66
67 if name.is_empty() {
69 if config.strict_empty {
70 return Err(ArchToolkitError::EmptyInput {
71 field: "package name".to_string(),
72 message: "package name cannot be empty".to_string(),
73 });
74 }
75 return Ok(name); }
77
78 if name.len() > config.max_package_name_length {
80 return Err(ArchToolkitError::InputTooLong {
81 field: "package name".to_string(),
82 max_length: config.max_package_name_length,
83 actual_length: name.len(),
84 });
85 }
86
87 if name.starts_with('-') {
89 return Err(ArchToolkitError::InvalidPackageName {
90 name: name.to_string(),
91 reason: "package name cannot start with a hyphen (-)".to_string(),
92 });
93 }
94
95 if name.starts_with('.') {
96 return Err(ArchToolkitError::InvalidPackageName {
97 name: name.to_string(),
98 reason: "package name cannot start with a period (.)".to_string(),
99 });
100 }
101
102 for (idx, ch) in name.char_indices() {
105 let is_valid = matches!(ch,
106 'a'..='z' | '0'..='9' | '@' | '.' | '_' | '+' | '-'
107 );
108
109 if !is_valid {
110 return Err(ArchToolkitError::InvalidPackageName {
111 name: name.to_string(),
112 reason: format!(
113 "package name contains invalid character '{ch}' at position {idx} (allowed: lowercase letters, digits, @, ., _, +, -)"
114 ),
115 });
116 }
117 }
118
119 Ok(name)
120}
121
122pub fn validate_package_names(names: &[&str], config: Option<&ValidationConfig>) -> Result<()> {
141 let config = config.unwrap_or(&DEFAULT_VALIDATION_CONFIG);
142
143 if names.is_empty() {
145 if config.strict_empty {
146 return Err(ArchToolkitError::EmptyInput {
147 field: "package names".to_string(),
148 message: "at least one package name is required".to_string(),
149 });
150 }
151 return Ok(()); }
153
154 for name in names {
156 validate_package_name(name, Some(config))?;
157 }
158
159 Ok(())
160}
161
162pub fn validate_search_query<'a>(
182 query: &'a str,
183 config: Option<&ValidationConfig>,
184) -> Result<&'a str> {
185 let config = config.unwrap_or(&DEFAULT_VALIDATION_CONFIG);
186 let trimmed = query.trim();
187
188 if trimmed.is_empty() {
190 if config.strict_empty {
191 return Err(ArchToolkitError::EmptyInput {
192 field: "search query".to_string(),
193 message: "search query cannot be empty".to_string(),
194 });
195 }
196 return Ok(trimmed); }
198
199 if trimmed.len() > config.max_query_length {
201 return Err(ArchToolkitError::InputTooLong {
202 field: "search query".to_string(),
203 max_length: config.max_query_length,
204 actual_length: trimmed.len(),
205 });
206 }
207
208 Ok(trimmed)
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214
215 #[test]
216 fn test_validate_package_name_valid() {
217 let valid_names = [
218 "yay",
219 "paru",
220 "linux-zen",
221 "lib32-mesa",
222 "python-numpy",
223 "gcc@12",
224 "package_name",
225 "pkg+plus",
226 "123package",
227 ];
228
229 for name in &valid_names {
230 assert!(
231 validate_package_name(name, None).is_ok(),
232 "Package name '{name}' should be valid"
233 );
234 }
235 }
236
237 #[test]
238 fn test_validate_package_name_empty() {
239 let result = validate_package_name("", None);
241 assert!(result.is_err());
242 match result.expect_err("Expected validation error") {
243 ArchToolkitError::EmptyInput { field, .. } => {
244 assert_eq!(field, "package name");
245 }
246 _ => panic!("Expected EmptyInput error"),
247 }
248
249 let config = ValidationConfig {
251 strict_empty: false,
252 ..Default::default()
253 };
254 assert!(validate_package_name("", Some(&config)).is_ok());
255 }
256
257 #[test]
258 fn test_validate_package_name_starts_with_hyphen() {
259 let result = validate_package_name("-invalid", None);
260 assert!(result.is_err());
261 match result.expect_err("Expected validation error") {
262 ArchToolkitError::InvalidPackageName { name, reason } => {
263 assert_eq!(name, "-invalid");
264 assert!(reason.contains("hyphen"));
265 }
266 _ => panic!("Expected InvalidPackageName error"),
267 }
268 }
269
270 #[test]
271 fn test_validate_package_name_starts_with_period() {
272 let result = validate_package_name(".invalid", None);
273 assert!(result.is_err());
274 match result.expect_err("Expected validation error") {
275 ArchToolkitError::InvalidPackageName { name, reason } => {
276 assert_eq!(name, ".invalid");
277 assert!(reason.contains("period"));
278 }
279 _ => panic!("Expected InvalidPackageName error"),
280 }
281 }
282
283 #[test]
284 fn test_validate_package_name_uppercase() {
285 let result = validate_package_name("Invalid", None);
286 assert!(result.is_err());
287 match result.expect_err("Expected validation error") {
288 ArchToolkitError::InvalidPackageName { name, reason } => {
289 assert_eq!(name, "Invalid");
290 assert!(reason.contains("invalid character"));
291 }
292 _ => panic!("Expected InvalidPackageName error"),
293 }
294 }
295
296 #[test]
297 fn test_validate_package_name_special_chars() {
298 let invalid = ["package#name", "package name", "package!"];
299
300 for name in &invalid {
301 let result = validate_package_name(name, None);
302 assert!(result.is_err(), "Package name '{name}' should be invalid");
303 match result.expect_err("Expected validation error") {
304 ArchToolkitError::InvalidPackageName { .. } => {}
305 _ => panic!("Expected InvalidPackageName error for '{name}'"),
306 }
307 }
308 }
309
310 #[test]
311 fn test_validate_package_name_too_long() {
312 let long_name = "a".repeat(128); let result = validate_package_name(&long_name, None);
314 assert!(result.is_err());
315 match result.expect_err("Expected validation error") {
316 ArchToolkitError::InputTooLong {
317 field,
318 max_length,
319 actual_length,
320 } => {
321 assert_eq!(field, "package name");
322 assert_eq!(max_length, 127);
323 assert_eq!(actual_length, 128);
324 }
325 _ => panic!("Expected InputTooLong error"),
326 }
327 }
328
329 #[test]
330 fn test_validate_package_name_custom_max_length() {
331 let config = ValidationConfig {
332 max_package_name_length: 10,
333 ..Default::default()
334 };
335 let name = "a".repeat(11);
336 let result = validate_package_name(&name, Some(&config));
337 assert!(result.is_err());
338 match result.expect_err("Expected validation error") {
339 ArchToolkitError::InputTooLong { max_length, .. } => {
340 assert_eq!(max_length, 10);
341 }
342 _ => panic!("Expected InputTooLong error"),
343 }
344 }
345
346 #[test]
347 fn test_validate_package_names_valid() {
348 let names = &["yay", "paru", "linux-zen"];
349 assert!(validate_package_names(names, None).is_ok());
350 }
351
352 #[test]
353 fn test_validate_package_names_empty() {
354 let result = validate_package_names(&[], None);
356 assert!(result.is_err());
357 match result.expect_err("Expected validation error") {
358 ArchToolkitError::EmptyInput { field, .. } => {
359 assert_eq!(field, "package names");
360 }
361 _ => panic!("Expected EmptyInput error"),
362 }
363
364 let config = ValidationConfig {
366 strict_empty: false,
367 ..Default::default()
368 };
369 assert!(validate_package_names(&[], Some(&config)).is_ok());
370 }
371
372 #[test]
373 fn test_validate_package_names_invalid() {
374 let names = &["yay", "-invalid", "paru"];
375 let result = validate_package_names(names, None);
376 assert!(result.is_err());
377 match result.expect_err("Expected validation error") {
378 ArchToolkitError::InvalidPackageName { name, .. } => {
379 assert_eq!(name, "-invalid");
380 }
381 _ => panic!("Expected InvalidPackageName error"),
382 }
383 }
384
385 #[test]
386 fn test_validate_search_query_valid() {
387 let queries = ["yay", "paru helper", "linux", " trimmed "];
388
389 for query in &queries {
390 let result = validate_search_query(query, None);
391 assert!(result.is_ok(), "Query '{query}' should be valid");
392 if let Ok(trimmed) = result {
394 assert_eq!(trimmed, query.trim());
395 }
396 }
397 }
398
399 #[test]
400 fn test_validate_search_query_empty() {
401 let result = validate_search_query("", None);
403 assert!(result.is_err());
404 match result.expect_err("Expected validation error") {
405 ArchToolkitError::EmptyInput { field, .. } => {
406 assert_eq!(field, "search query");
407 }
408 _ => panic!("Expected EmptyInput error"),
409 }
410
411 let result = validate_search_query(" ", None);
413 assert!(result.is_err());
414
415 let config = ValidationConfig {
417 strict_empty: false,
418 ..Default::default()
419 };
420 assert!(validate_search_query("", Some(&config)).is_ok());
421 assert!(validate_search_query(" ", Some(&config)).is_ok());
422 }
423
424 #[test]
425 fn test_validate_search_query_too_long() {
426 let long_query = "a".repeat(257); let result = validate_search_query(&long_query, None);
428 assert!(result.is_err());
429 match result.expect_err("Expected validation error") {
430 ArchToolkitError::InputTooLong {
431 field,
432 max_length,
433 actual_length,
434 } => {
435 assert_eq!(field, "search query");
436 assert_eq!(max_length, 256);
437 assert_eq!(actual_length, 257);
438 }
439 _ => panic!("Expected InputTooLong error"),
440 }
441 }
442
443 #[test]
444 fn test_validate_search_query_custom_max_length() {
445 let config = ValidationConfig {
446 max_query_length: 10,
447 ..Default::default()
448 };
449 let query = "a".repeat(11);
450 let result = validate_search_query(&query, Some(&config));
451 assert!(result.is_err());
452 match result.expect_err("Expected validation error") {
453 ArchToolkitError::InputTooLong { max_length, .. } => {
454 assert_eq!(max_length, 10);
455 }
456 _ => panic!("Expected InputTooLong error"),
457 }
458 }
459
460 #[test]
461 fn test_validation_config_default() {
462 let config = ValidationConfig::default();
463 assert!(config.strict_empty);
464 assert_eq!(config.max_query_length, 256);
465 assert_eq!(config.max_package_name_length, 127);
466 }
467}