1use async_trait::async_trait;
2use rand::Rng;
3use std::fs;
4use std::path::{Path, PathBuf};
5use walkdir::DirEntry;
6
7pub fn read_gitignore_patterns(base_dir: &str) -> Vec<String> {
9 let mut patterns = vec![".git".to_string()]; let gitignore_path = PathBuf::from(base_dir).join(".gitignore");
12 if let Ok(content) = std::fs::read_to_string(&gitignore_path) {
13 for line in content.lines() {
14 let line = line.trim();
15 if !line.is_empty() && !line.starts_with('#') {
17 patterns.push(line.to_string());
18 }
19 }
20 }
21
22 patterns
23}
24
25pub fn should_include_entry(entry: &DirEntry, base_dir: &str, ignore_patterns: &[String]) -> bool {
27 let path = entry.path();
28 let is_file = entry.file_type().is_file();
29
30 let base_path = PathBuf::from(base_dir);
32 let relative_path = match path.strip_prefix(&base_path) {
33 Ok(rel_path) => rel_path,
34 Err(_) => path,
35 };
36
37 let path_str = relative_path.to_string_lossy();
38
39 for pattern in ignore_patterns {
41 if matches_gitignore_pattern(pattern, &path_str) {
42 return false;
43 }
44 }
45
46 if is_file {
48 is_supported_file(entry.path())
49 } else {
50 true }
52}
53
54pub fn matches_gitignore_pattern(pattern: &str, path: &str) -> bool {
56 let pattern = pattern.trim_end_matches('/'); if pattern.contains('*') {
60 if pattern == "*" {
61 true
62 } else if pattern.starts_with('*') && pattern.ends_with('*') {
63 let middle = &pattern[1..pattern.len() - 1];
64 path.contains(middle)
65 } else if let Some(suffix) = pattern.strip_prefix('*') {
66 path.ends_with(suffix)
67 } else if let Some(prefix) = pattern.strip_suffix('*') {
68 path.starts_with(prefix)
69 } else {
70 pattern_matches_glob(pattern, path)
72 }
73 } else {
74 path == pattern || path.starts_with(&format!("{}/", pattern))
76 }
77}
78
79pub fn pattern_matches_glob(pattern: &str, text: &str) -> bool {
81 let parts: Vec<&str> = pattern.split('*').collect();
82 if parts.len() == 1 {
83 return text == pattern;
84 }
85
86 let mut text_pos = 0;
87 for (i, part) in parts.iter().enumerate() {
88 if i == 0 {
89 if !text[text_pos..].starts_with(part) {
91 return false;
92 }
93 text_pos += part.len();
94 } else if i == parts.len() - 1 {
95 return text[text_pos..].ends_with(part);
97 } else {
98 if let Some(pos) = text[text_pos..].find(part) {
100 text_pos += pos + part.len();
101 } else {
102 return false;
103 }
104 }
105 }
106 true
107}
108
109pub fn is_supported_file(file_path: &Path) -> bool {
111 match file_path.file_name().and_then(|name| name.to_str()) {
112 Some(name) => {
113 if file_path.is_file() {
115 name.ends_with(".tf")
116 || name.ends_with(".tfvars")
117 || name.ends_with(".yaml")
118 || name.ends_with(".yml")
119 || name.to_lowercase().contains("dockerfile")
120 } else {
121 true }
123 }
124 None => false,
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131 use std::fs;
132 use std::io::Write;
133 use tempfile::TempDir;
134
135 #[test]
136 fn test_matches_gitignore_pattern_exact() {
137 assert!(matches_gitignore_pattern("node_modules", "node_modules"));
138 assert!(matches_gitignore_pattern(
139 "node_modules",
140 "node_modules/package.json"
141 ));
142 assert!(!matches_gitignore_pattern(
143 "node_modules",
144 "src/node_modules"
145 ));
146 }
147
148 #[test]
149 fn test_matches_gitignore_pattern_wildcard_prefix() {
150 assert!(matches_gitignore_pattern("*.log", "debug.log"));
151 assert!(matches_gitignore_pattern("*.log", "error.log"));
152 assert!(!matches_gitignore_pattern("*.log", "log.txt"));
153 }
154
155 #[test]
156 fn test_matches_gitignore_pattern_wildcard_suffix() {
157 assert!(matches_gitignore_pattern("temp*", "temp"));
158 assert!(matches_gitignore_pattern("temp*", "temp.txt"));
159 assert!(matches_gitignore_pattern("temp*", "temporary"));
160 assert!(!matches_gitignore_pattern("temp*", "mytemp"));
161 }
162
163 #[test]
164 fn test_matches_gitignore_pattern_wildcard_middle() {
165 assert!(matches_gitignore_pattern("*temp*", "temp"));
166 assert!(matches_gitignore_pattern("*temp*", "mytemp"));
167 assert!(matches_gitignore_pattern("*temp*", "temporary"));
168 assert!(matches_gitignore_pattern("*temp*", "mytemporary"));
169 assert!(!matches_gitignore_pattern("*temp*", "example"));
170 }
171
172 #[test]
173 fn test_pattern_matches_glob() {
174 assert!(pattern_matches_glob("test*.txt", "test.txt"));
175 assert!(pattern_matches_glob("test*.txt", "test123.txt"));
176 assert!(pattern_matches_glob("*test*.txt", "mytest.txt"));
177 assert!(pattern_matches_glob("*test*.txt", "mytestfile.txt"));
178 assert!(!pattern_matches_glob("test*.txt", "test.log"));
179 assert!(!pattern_matches_glob("*test*.txt", "example.txt"));
180 }
181
182 #[test]
183 fn test_read_gitignore_patterns() -> Result<(), Box<dyn std::error::Error>> {
184 let temp_dir = TempDir::new()?;
185 let temp_path = temp_dir.path();
186
187 let gitignore_content = r#"
189# This is a comment
190node_modules
191*.log
192dist/
193.env
194
195# Another comment
196temp*
197"#;
198
199 let gitignore_path = temp_path.join(".gitignore");
200 let mut file = fs::File::create(&gitignore_path)?;
201 file.write_all(gitignore_content.as_bytes())?;
202
203 let patterns = read_gitignore_patterns(temp_path.to_str().unwrap());
204
205 assert!(patterns.contains(&".git".to_string()));
207 assert!(patterns.contains(&"node_modules".to_string()));
208 assert!(patterns.contains(&"*.log".to_string()));
209 assert!(patterns.contains(&"dist/".to_string()));
210 assert!(patterns.contains(&".env".to_string()));
211 assert!(patterns.contains(&"temp*".to_string()));
212
213 assert!(!patterns.iter().any(|p| p.starts_with('#')));
215 assert!(!patterns.contains(&"".to_string()));
216
217 Ok(())
218 }
219
220 #[test]
221 fn test_read_gitignore_patterns_no_file() {
222 let temp_dir = TempDir::new().unwrap();
223 let temp_path = temp_dir.path();
224
225 let patterns = read_gitignore_patterns(temp_path.to_str().unwrap());
226
227 assert_eq!(patterns, vec![".git".to_string()]);
229 }
230
231 #[test]
232 fn test_gitignore_integration() -> Result<(), Box<dyn std::error::Error>> {
233 let temp_dir = TempDir::new()?;
234 let temp_path = temp_dir.path();
235
236 let gitignore_content = "node_modules\n*.log\ndist/\n";
238 let gitignore_path = temp_path.join(".gitignore");
239 let mut file = fs::File::create(&gitignore_path)?;
240 file.write_all(gitignore_content.as_bytes())?;
241
242 let patterns = read_gitignore_patterns(temp_path.to_str().unwrap());
243
244 assert!(
246 patterns
247 .iter()
248 .any(|p| matches_gitignore_pattern(p, "node_modules"))
249 );
250 assert!(
251 patterns
252 .iter()
253 .any(|p| matches_gitignore_pattern(p, "node_modules/package.json"))
254 );
255 assert!(
256 patterns
257 .iter()
258 .any(|p| matches_gitignore_pattern(p, "debug.log"))
259 );
260 assert!(
261 patterns
262 .iter()
263 .any(|p| matches_gitignore_pattern(p, "dist/bundle.js"))
264 );
265 assert!(
266 patterns
267 .iter()
268 .any(|p| matches_gitignore_pattern(p, ".git"))
269 );
270
271 assert!(
273 !patterns
274 .iter()
275 .any(|p| matches_gitignore_pattern(p, "src/main.js"))
276 );
277 assert!(
278 !patterns
279 .iter()
280 .any(|p| matches_gitignore_pattern(p, "README.md"))
281 );
282
283 Ok(())
284 }
285}
286
287pub fn generate_password(length: usize, no_symbols: bool) -> String {
289 let mut rng = rand::rng();
290
291 let lowercase = "abcdefghijklmnopqrstuvwxyz";
293 let uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
294 let digits = "0123456789";
295 let symbols = "!@#$%^&*()_+-=[]{}|;:,.<>?";
296
297 let mut charset = String::new();
299 charset.push_str(lowercase);
300 charset.push_str(uppercase);
301 charset.push_str(digits);
302
303 if !no_symbols {
304 charset.push_str(symbols);
305 }
306
307 let charset_chars: Vec<char> = charset.chars().collect();
308
309 let mut password = String::new();
311
312 password.push(
314 lowercase
315 .chars()
316 .nth(rng.random_range(0..lowercase.len()))
317 .unwrap(),
318 );
319 password.push(
320 uppercase
321 .chars()
322 .nth(rng.random_range(0..uppercase.len()))
323 .unwrap(),
324 );
325 password.push(
326 digits
327 .chars()
328 .nth(rng.random_range(0..digits.len()))
329 .unwrap(),
330 );
331
332 if !no_symbols {
333 password.push(
334 symbols
335 .chars()
336 .nth(rng.random_range(0..symbols.len()))
337 .unwrap(),
338 );
339 }
340
341 let remaining_length = if length > password.len() {
343 length - password.len()
344 } else {
345 0
346 };
347
348 for _ in 0..remaining_length {
349 let random_char = charset_chars[rng.random_range(0..charset_chars.len())];
350 password.push(random_char);
351 }
352
353 let mut password_chars: Vec<char> = password.chars().collect();
355 for i in 0..password_chars.len() {
356 let j = rng.random_range(0..password_chars.len());
357 password_chars.swap(i, j);
358 }
359
360 password_chars.into_iter().take(length).collect()
362}
363
364#[cfg(test)]
365mod password_tests {
366 use super::*;
367
368 #[test]
369 fn test_generate_password_length() {
370 let password = generate_password(10, false);
371 assert_eq!(password.len(), 10);
372
373 let password = generate_password(20, true);
374 assert_eq!(password.len(), 20);
375 }
376
377 #[test]
378 fn test_generate_password_no_symbols() {
379 let password = generate_password(50, true);
380 let symbols = "!@#$%^&*()_+-=[]{}|;:,.<>?";
381
382 for symbol in symbols.chars() {
383 assert!(
384 !password.contains(symbol),
385 "Password should not contain symbol: {}",
386 symbol
387 );
388 }
389 }
390
391 #[test]
392 fn test_generate_password_with_symbols() {
393 let password = generate_password(50, false);
394 let symbols = "!@#$%^&*()_+-=[]{}|;:,.<>?";
395
396 let has_symbol = password.chars().any(|c| symbols.contains(c));
398 assert!(has_symbol, "Password should contain at least one symbol");
399 }
400
401 #[test]
402 fn test_generate_password_contains_required_chars() {
403 let password = generate_password(50, false);
404
405 let has_lowercase = password.chars().any(|c| c.is_ascii_lowercase());
406 let has_uppercase = password.chars().any(|c| c.is_ascii_uppercase());
407 let has_digit = password.chars().any(|c| c.is_ascii_digit());
408
409 assert!(has_lowercase, "Password should contain lowercase letters");
410 assert!(has_uppercase, "Password should contain uppercase letters");
411 assert!(has_digit, "Password should contain digits");
412 }
413
414 #[test]
415 fn test_generate_password_uniqueness() {
416 let password1 = generate_password(20, false);
417 let password2 = generate_password(20, false);
418
419 assert_ne!(password1, password2);
421 }
422}
423
424#[derive(Debug, Clone)]
426pub struct DirectoryEntry {
427 pub name: String,
428 pub path: String,
429 pub is_directory: bool,
430}
431
432#[async_trait]
434pub trait FileSystemProvider {
435 type Error: std::fmt::Display;
436
437 async fn list_directory(&self, path: &str) -> Result<Vec<DirectoryEntry>, Self::Error>;
439}
440
441pub async fn generate_directory_tree<P: FileSystemProvider>(
443 provider: &P,
444 path: &str,
445 prefix: &str,
446 max_depth: usize,
447 current_depth: usize,
448) -> Result<String, P::Error> {
449 let mut result = String::new();
450
451 if current_depth >= max_depth || current_depth >= 10 {
452 return Ok(result);
453 }
454
455 let entries = provider.list_directory(path).await?;
456 let mut file_entries = Vec::new();
457 let mut dir_entries = Vec::new();
458 for entry in entries.iter() {
459 if entry.is_directory {
460 if entry.name == "."
461 || entry.name == ".."
462 || entry.name == ".git"
463 || entry.name == "node_modules"
464 {
465 continue;
466 }
467 dir_entries.push(entry.clone());
468 } else {
469 file_entries.push(entry.clone());
470 }
471 }
472
473 dir_entries.sort_by(|a, b| a.name.cmp(&b.name));
474 file_entries.sort_by(|a, b| a.name.cmp(&b.name));
475
476 const MAX_ITEMS: usize = 5;
477 let total_items = dir_entries.len() + file_entries.len();
478 let should_limit = current_depth > 0 && total_items > MAX_ITEMS;
479
480 if should_limit {
481 if dir_entries.len() > MAX_ITEMS {
482 dir_entries.truncate(MAX_ITEMS);
483 file_entries.clear();
484 } else {
485 let remaining_items = MAX_ITEMS - dir_entries.len();
486 file_entries.truncate(remaining_items);
487 }
488 }
489
490 let mut dir_headers = Vec::new();
491 let mut dir_futures = Vec::new();
492 for (i, entry) in dir_entries.iter().enumerate() {
493 let is_last_dir = i == dir_entries.len() - 1;
494 let is_last_overall = is_last_dir && file_entries.is_empty() && !should_limit;
495 let current_prefix = if is_last_overall {
496 "└── "
497 } else {
498 "├── "
499 };
500 let next_prefix = format!(
501 "{}{}",
502 prefix,
503 if is_last_overall { " " } else { "│ " }
504 );
505
506 let header = format!("{}{}{}/\n", prefix, current_prefix, entry.name);
507 dir_headers.push(header);
508
509 let entry_path = entry.path.clone();
510 let next_prefix_clone = next_prefix.clone();
511 let future = async move {
512 generate_directory_tree(
513 provider,
514 &entry_path,
515 &next_prefix_clone,
516 max_depth,
517 current_depth + 1,
518 )
519 .await
520 };
521 dir_futures.push(future);
522 }
523 if !dir_futures.is_empty() {
524 let subtree_results = futures::future::join_all(dir_futures).await;
525
526 for (i, header) in dir_headers.iter().enumerate() {
527 result.push_str(header);
528 if let Some(Ok(subtree)) = subtree_results.get(i) {
529 result.push_str(subtree);
530 }
531 }
532 }
533
534 for (i, entry) in file_entries.iter().enumerate() {
535 let is_last_file = i == file_entries.len() - 1;
536 let is_last_overall = is_last_file && !should_limit;
537 let current_prefix = if is_last_overall {
538 "└── "
539 } else {
540 "├── "
541 };
542 result.push_str(&format!("{}{}{}\n", prefix, current_prefix, entry.name));
543 }
544
545 if should_limit {
546 let remaining_count = total_items - MAX_ITEMS;
547 result.push_str(&format!(
548 "{}└── ... {} more item{}\n",
549 prefix,
550 remaining_count,
551 if remaining_count == 1 { "" } else { "s" }
552 ));
553 }
554
555 Ok(result)
556}
557
558pub struct LocalFileSystemProvider;
560
561#[async_trait]
562impl FileSystemProvider for LocalFileSystemProvider {
563 type Error = std::io::Error;
564
565 async fn list_directory(&self, path: &str) -> Result<Vec<DirectoryEntry>, Self::Error> {
566 let entries = fs::read_dir(path)?;
567 let mut result = Vec::new();
568
569 for entry in entries {
570 let entry = entry?;
571 let file_name = entry.file_name().to_string_lossy().to_string();
572 let file_path = entry.path().to_string_lossy().to_string();
573 let is_directory = entry.file_type()?.is_dir();
574
575 result.push(DirectoryEntry {
576 name: file_name,
577 path: file_path,
578 is_directory,
579 });
580 }
581
582 Ok(result)
583 }
584}