1use ignore::WalkBuilder;
2use std::path::Path;
3
4pub struct DirectoryStructureUtils;
6
7impl DirectoryStructureUtils {
8 pub fn get_directory_structure(
11 root_path: &Path,
12 max_depth: usize,
13 max_items: Option<usize>,
14 ) -> Result<String, std::io::Error> {
15 let mut structure = vec![root_path.display().to_string()];
16
17 let (paths, truncated) = Self::collect_directory_paths(root_path, max_depth, max_items)?;
19 structure.extend(paths);
20
21 structure.sort();
22 let mut result = structure.join("\n");
23
24 if truncated > 0 {
25 result.push_str(&format!("\n... and {truncated} more items"));
26 }
27
28 Ok(result)
29 }
30
31 fn collect_directory_paths(
34 root_path: &Path,
35 max_depth: usize,
36 max_items: Option<usize>,
37 ) -> Result<(Vec<String>, usize), std::io::Error> {
38 let mut paths = Vec::new();
39 let mut item_count = 0;
40 let mut truncated = 0;
41 let limit = max_items.unwrap_or(usize::MAX);
42 let mut walker_seen_dirs = std::collections::HashSet::new();
43
44 let walker = WalkBuilder::new(root_path)
47 .max_depth(Some(max_depth))
48 .hidden(true) .build();
50
51 for entry in walker {
52 let entry = match entry {
53 Ok(e) => e,
54 Err(_) => continue,
55 };
56
57 if entry.path() == root_path {
59 continue;
60 }
61
62 if let Ok(relative_path) = entry.path().strip_prefix(root_path)
63 && let Some(path_str) = relative_path.to_str()
64 && !path_str.is_empty()
65 {
66 if entry.depth() == 1
68 && entry.file_type().is_some_and(|ft| ft.is_dir())
69 && let Some(dir_name) = relative_path.file_name()
70 {
71 walker_seen_dirs.insert(dir_name.to_string_lossy().to_string());
72 }
73
74 if item_count >= limit {
75 truncated += 1;
76 continue;
77 }
78
79 if entry.file_type().is_some_and(|ft| ft.is_dir()) {
80 paths.push(format!("{path_str}/"));
81 } else {
82 paths.push(path_str.to_string());
83 }
84 item_count += 1;
85 }
86 }
87
88 if max_depth > 0 {
91 let entries = std::fs::read_dir(root_path)?;
92 for entry in entries {
93 let entry = match entry {
94 Ok(e) => e,
95 Err(_) => continue,
96 };
97
98 let path = entry.path();
99 if !path.is_dir() {
100 continue;
101 }
102
103 let file_name = match path.file_name() {
104 Some(name) => name.to_string_lossy().to_string(),
105 None => continue,
106 };
107
108 if walker_seen_dirs.contains(&file_name) {
110 continue;
111 }
112
113 if item_count >= limit {
115 truncated += 1;
116 continue;
117 }
118
119 let dir_item_count = Self::count_items_in_dir(&path);
121 if dir_item_count > 0 {
122 paths.push(format!("{file_name}/ ({dir_item_count} items)"));
123 } else {
124 paths.push(format!("{file_name}/ (empty)"));
125 }
126 item_count += 1;
127 }
128 }
129
130 Ok((paths, truncated))
131 }
132
133 fn count_items_in_dir(dir: &Path) -> usize {
135 std::fs::read_dir(dir)
136 .map(|entries| entries.count())
137 .unwrap_or(0)
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144 use tempfile::tempdir;
145
146 #[test]
147 #[cfg(unix)]
148 fn test_directory_structure_skips_inaccessible() {
149 use std::os::unix::fs::PermissionsExt;
150
151 let temp_dir = tempdir().unwrap();
152
153 let accessible_dir = temp_dir.path().join("accessible");
155 std::fs::create_dir(&accessible_dir).unwrap();
156 std::fs::write(accessible_dir.join("file.txt"), "test").unwrap();
157
158 let restricted_dir = temp_dir.path().join("restricted");
160 std::fs::create_dir(&restricted_dir).unwrap();
161 std::fs::write(restricted_dir.join("hidden.txt"), "secret").unwrap();
162
163 let mut perms = std::fs::metadata(&restricted_dir).unwrap().permissions();
165 perms.set_mode(0o000);
166 std::fs::set_permissions(&restricted_dir, perms).unwrap();
167
168 let result =
170 DirectoryStructureUtils::get_directory_structure(temp_dir.path(), 3, None).unwrap();
171
172 assert!(result.contains("accessible/"));
174
175 let mut perms = std::fs::metadata(&restricted_dir).unwrap().permissions();
177 perms.set_mode(0o755);
178 std::fs::set_permissions(&restricted_dir, perms).unwrap();
179 }
180
181 #[test]
182 fn test_directory_structure_empty_dir() {
183 let temp_dir = tempdir().unwrap();
184 let expected = temp_dir.path().display().to_string();
185 let result =
186 DirectoryStructureUtils::get_directory_structure(temp_dir.path(), 3, None).unwrap();
187 assert_eq!(result, expected);
188 }
189
190 #[test]
191 fn test_directory_structure_with_gitignored_dirs() {
192 let temp_dir = tempdir().unwrap();
193
194 std::fs::write(
196 temp_dir.path().join(".gitignore"),
197 "target/\nnode_modules/\n*.log",
198 )
199 .unwrap();
200
201 std::fs::create_dir(temp_dir.path().join("src")).unwrap();
203 std::fs::write(temp_dir.path().join("src/main.rs"), "main").unwrap();
204 std::fs::write(temp_dir.path().join("Cargo.toml"), "cargo").unwrap();
205
206 std::fs::create_dir(temp_dir.path().join("target")).unwrap();
208 std::fs::create_dir(temp_dir.path().join("target/debug")).unwrap();
209 std::fs::write(temp_dir.path().join("target/debug/app"), "binary").unwrap();
210
211 std::fs::create_dir(temp_dir.path().join("node_modules")).unwrap();
212 std::fs::create_dir(temp_dir.path().join("node_modules/pkg1")).unwrap();
213 std::fs::create_dir(temp_dir.path().join("node_modules/pkg2")).unwrap();
214 std::fs::write(temp_dir.path().join("node_modules/pkg1/index.js"), "js").unwrap();
215
216 std::fs::create_dir(temp_dir.path().join(".git")).unwrap();
218 std::fs::write(temp_dir.path().join(".git/config"), "config").unwrap();
219 std::fs::write(temp_dir.path().join(".git/HEAD"), "HEAD").unwrap();
220
221 std::fs::write(temp_dir.path().join("debug.log"), "log").unwrap();
223
224 let mut expected_lines = [
228 temp_dir.path().display().to_string(),
229 ".git/ (2 items)".to_string(), "Cargo.toml".to_string(),
231 "node_modules/ (2 items)".to_string(), "src/".to_string(),
233 "src/main.rs".to_string(),
234 "target/ (1 items)".to_string(), ];
236 expected_lines.sort();
237 let expected = expected_lines.join("\n");
238
239 let result =
240 DirectoryStructureUtils::get_directory_structure(temp_dir.path(), 3, None).unwrap();
241 assert_eq!(result, expected);
242 }
243
244 #[test]
245 fn test_directory_structure_with_files() {
246 let temp_dir = tempdir().unwrap();
247
248 std::fs::write(temp_dir.path().join("file1.txt"), "content1").unwrap();
250 std::fs::write(temp_dir.path().join("file2.rs"), "content2").unwrap();
251
252 let mut expected_lines = [
253 temp_dir.path().display().to_string(),
254 "file1.txt".to_string(),
255 "file2.rs".to_string(),
256 ];
257 expected_lines.sort();
258 let expected = expected_lines.join("\n");
259
260 let result =
261 DirectoryStructureUtils::get_directory_structure(temp_dir.path(), 3, None).unwrap();
262 assert_eq!(result, expected);
263 }
264
265 #[test]
266 fn test_directory_structure_with_subdirs() {
267 let temp_dir = tempdir().unwrap();
268
269 std::fs::create_dir(temp_dir.path().join("src")).unwrap();
271 std::fs::create_dir(temp_dir.path().join("tests")).unwrap();
272 std::fs::write(temp_dir.path().join("src/main.rs"), "main").unwrap();
273 std::fs::write(temp_dir.path().join("tests/test.rs"), "test").unwrap();
274
275 let mut expected_lines = [
276 temp_dir.path().display().to_string(),
277 "src/".to_string(),
278 "src/main.rs".to_string(),
279 "tests/".to_string(),
280 "tests/test.rs".to_string(),
281 ];
282 expected_lines.sort();
283 let expected = expected_lines.join("\n");
284
285 let result =
286 DirectoryStructureUtils::get_directory_structure(temp_dir.path(), 3, None).unwrap();
287 assert_eq!(result, expected);
288 }
289
290 #[test]
291 fn test_directory_structure_max_depth_zero() {
292 let temp_dir = tempdir().unwrap();
293
294 std::fs::create_dir(temp_dir.path().join("src")).unwrap();
296 std::fs::write(temp_dir.path().join("src/lib.rs"), "lib").unwrap();
297
298 let expected = temp_dir.path().display().to_string();
299 let result =
300 DirectoryStructureUtils::get_directory_structure(temp_dir.path(), 0, None).unwrap();
301 assert_eq!(result, expected);
302 }
303
304 #[test]
305 fn test_directory_structure_max_depth_one() {
306 let temp_dir = tempdir().unwrap();
307
308 std::fs::create_dir(temp_dir.path().join("src")).unwrap();
310 std::fs::create_dir(temp_dir.path().join("src/nested")).unwrap();
311 std::fs::write(temp_dir.path().join("file.txt"), "root file").unwrap();
312 std::fs::write(temp_dir.path().join("src/lib.rs"), "lib").unwrap();
313 std::fs::write(temp_dir.path().join("src/nested/deep.rs"), "deep").unwrap();
314
315 let mut expected_lines = [
317 temp_dir.path().display().to_string(),
318 "file.txt".to_string(),
319 "src/".to_string(),
320 ];
321 expected_lines.sort();
322 let expected = expected_lines.join("\n");
323
324 let result =
325 DirectoryStructureUtils::get_directory_structure(temp_dir.path(), 1, None).unwrap();
326 assert_eq!(result, expected);
327 }
328
329 #[test]
330 fn test_directory_structure_deeply_nested() {
331 let temp_dir = tempdir().unwrap();
332
333 std::fs::create_dir(temp_dir.path().join("a")).unwrap();
335 std::fs::create_dir(temp_dir.path().join("a/b")).unwrap();
336 std::fs::create_dir(temp_dir.path().join("a/b/c")).unwrap();
337 std::fs::write(temp_dir.path().join("a/file1.txt"), "1").unwrap();
338 std::fs::write(temp_dir.path().join("a/b/file2.txt"), "2").unwrap();
339 std::fs::write(temp_dir.path().join("a/b/c/file3.txt"), "3").unwrap();
340
341 let mut expected_lines = [
344 temp_dir.path().display().to_string(),
345 "a/".to_string(),
346 "a/b/".to_string(),
347 "a/file1.txt".to_string(),
348 ];
349 expected_lines.sort();
350 let expected = expected_lines.join("\n");
351
352 let result =
353 DirectoryStructureUtils::get_directory_structure(temp_dir.path(), 2, None).unwrap();
354 assert_eq!(result, expected);
355 }
356
357 #[test]
358 fn test_directory_structure_mixed_content() {
359 let temp_dir = tempdir().unwrap();
360
361 std::fs::write(temp_dir.path().join("README.md"), "readme").unwrap();
363 std::fs::write(temp_dir.path().join("Cargo.toml"), "cargo").unwrap();
364 std::fs::create_dir(temp_dir.path().join("src")).unwrap();
365 std::fs::create_dir(temp_dir.path().join("tests")).unwrap();
366 std::fs::create_dir(temp_dir.path().join(".git")).unwrap();
367 std::fs::write(temp_dir.path().join("src/lib.rs"), "lib").unwrap();
368 std::fs::write(temp_dir.path().join("src/main.rs"), "main").unwrap();
369 std::fs::write(temp_dir.path().join("tests/integration.rs"), "test").unwrap();
370 std::fs::write(temp_dir.path().join(".git/config"), "config").unwrap();
371
372 let mut expected_lines = vec![
374 temp_dir.path().display().to_string(),
375 ".git/ (1 items)".to_string(), "Cargo.toml".to_string(),
377 "README.md".to_string(),
378 "src/".to_string(),
379 "src/lib.rs".to_string(),
380 "src/main.rs".to_string(),
381 "tests/".to_string(),
382 "tests/integration.rs".to_string(),
383 ];
384 expected_lines.sort();
385 let expected = expected_lines.join("\n");
386
387 let result =
388 DirectoryStructureUtils::get_directory_structure(temp_dir.path(), 3, None).unwrap();
389 assert_eq!(result, expected);
390 }
391
392 #[test]
393 fn test_directory_structure_with_hidden_files() {
394 let temp_dir = tempdir().unwrap();
395
396 std::fs::write(temp_dir.path().join("README.md"), "readme").unwrap();
398 std::fs::write(temp_dir.path().join(".env"), "secrets").unwrap(); std::fs::write(temp_dir.path().join(".gitignore"), "*.log").unwrap(); std::fs::create_dir(temp_dir.path().join("src")).unwrap();
402 std::fs::write(temp_dir.path().join("src/main.rs"), "main").unwrap();
403
404 std::fs::create_dir(temp_dir.path().join(".cache")).unwrap(); std::fs::write(temp_dir.path().join(".cache/data"), "cached").unwrap();
406
407 std::fs::create_dir(temp_dir.path().join(".hidden")).unwrap(); std::fs::create_dir(temp_dir.path().join(".hidden/nested")).unwrap();
409 std::fs::write(temp_dir.path().join(".hidden/file.txt"), "hidden").unwrap();
410
411 let mut expected_lines = [
414 temp_dir.path().display().to_string(),
415 ".cache/ (1 items)".to_string(), ".hidden/ (2 items)".to_string(), "README.md".to_string(),
419 "src/".to_string(),
420 "src/main.rs".to_string(),
421 ];
422 expected_lines.sort();
423 let expected = expected_lines.join("\n");
424
425 let result =
426 DirectoryStructureUtils::get_directory_structure(temp_dir.path(), 3, None).unwrap();
427 assert_eq!(result, expected);
428 }
429
430 #[test]
431 fn test_directory_structure_special_chars() {
432 let temp_dir = tempdir().unwrap();
433
434 std::fs::write(temp_dir.path().join("file with spaces.txt"), "content").unwrap();
436 std::fs::write(temp_dir.path().join("file-with-dashes.rs"), "content").unwrap();
437 std::fs::write(temp_dir.path().join("file_with_underscores.md"), "content").unwrap();
438 std::fs::create_dir(temp_dir.path().join("dir with spaces")).unwrap();
439
440 let mut expected_lines = [
441 temp_dir.path().display().to_string(),
442 "dir with spaces/".to_string(),
443 "file with spaces.txt".to_string(),
444 "file-with-dashes.rs".to_string(),
445 "file_with_underscores.md".to_string(),
446 ];
447 expected_lines.sort();
448 let expected = expected_lines.join("\n");
449
450 let result =
451 DirectoryStructureUtils::get_directory_structure(temp_dir.path(), 3, None).unwrap();
452 assert_eq!(result, expected);
453 }
454
455 #[test]
456 fn test_directory_structure_with_max_items_limit() {
457 let temp_dir = tempdir().unwrap();
458
459 for i in 0..20 {
461 std::fs::write(temp_dir.path().join(format!("file{i:02}.txt")), "content").unwrap();
462 }
463
464 let result =
466 DirectoryStructureUtils::get_directory_structure(temp_dir.path(), 3, Some(5)).unwrap();
467
468 let lines: Vec<&str> = result.lines().collect();
469
470 assert_eq!(lines[0], temp_dir.path().display().to_string());
472 assert_eq!(lines.len(), 7); assert_eq!(lines[6], "... and 15 more items");
474
475 for line in lines.iter().take(6).skip(1) {
477 assert!(line.ends_with(".txt"));
478 }
479 }
480
481 #[test]
482 fn test_directory_structure_with_dirs_and_max_items() {
483 let temp_dir = tempdir().unwrap();
484
485 std::fs::create_dir(temp_dir.path().join("dir1")).unwrap();
487 std::fs::create_dir(temp_dir.path().join("dir2")).unwrap();
488 std::fs::write(temp_dir.path().join("file1.txt"), "content").unwrap();
489 std::fs::write(temp_dir.path().join("file2.txt"), "content").unwrap();
490 std::fs::create_dir(temp_dir.path().join("dir3")).unwrap();
491
492 let result =
494 DirectoryStructureUtils::get_directory_structure(temp_dir.path(), 3, Some(3)).unwrap();
495
496 let expected = format!(
497 "{}\ndir2/\nfile1.txt\nfile2.txt\n... and 2 more items",
498 temp_dir.path().display()
499 );
500
501 assert_eq!(result, expected);
502 }
503
504 #[test]
505 fn test_directory_structure_no_truncation_when_under_limit() {
506 let temp_dir = tempdir().unwrap();
507
508 std::fs::write(temp_dir.path().join("file1.txt"), "content").unwrap();
510 std::fs::write(temp_dir.path().join("file2.txt"), "content").unwrap();
511 std::fs::create_dir(temp_dir.path().join("subdir")).unwrap();
512
513 let result =
515 DirectoryStructureUtils::get_directory_structure(temp_dir.path(), 3, Some(100))
516 .unwrap();
517
518 assert!(!result.contains("... and"));
520 assert!(!result.contains("more items"));
521
522 let lines: Vec<&str> = result.lines().collect();
523 assert_eq!(lines.len(), 4); }
525
526 #[test]
527 fn test_directory_structure_with_hidden_dirs_and_limit() {
528 let temp_dir = tempdir().unwrap();
529
530 for i in 0..5 {
532 std::fs::write(temp_dir.path().join(format!("file{i}.txt")), "content").unwrap();
533 }
534
535 std::fs::create_dir(temp_dir.path().join(".hidden1")).unwrap();
537 std::fs::write(temp_dir.path().join(".hidden1/file.txt"), "hidden").unwrap();
538
539 std::fs::create_dir(temp_dir.path().join(".hidden2")).unwrap();
540 std::fs::write(temp_dir.path().join(".hidden2/file.txt"), "hidden").unwrap();
541
542 let result =
544 DirectoryStructureUtils::get_directory_structure(temp_dir.path(), 3, Some(4)).unwrap();
545
546 let lines: Vec<&str> = result.lines().collect();
547
548 assert_eq!(lines[0], temp_dir.path().display().to_string());
550 assert_eq!(lines.len(), 6); assert_eq!(lines[5], "... and 3 more items");
552
553 for line in lines.iter().take(5).skip(1) {
556 assert!(line.ends_with(".txt"));
557 }
558 }
559}