1use crate::diagnostics::{CoreError, FileError, LintResult};
55use std::collections::HashMap;
56use std::fs::Metadata;
57use std::io;
58use std::path::{Path, PathBuf};
59use std::sync::RwLock;
60
61#[derive(Debug, Clone)]
65pub struct FileMetadata {
66 pub is_file: bool,
68 pub is_dir: bool,
70 pub is_symlink: bool,
72 pub len: u64,
74}
75
76impl FileMetadata {
77 pub fn file(len: u64) -> Self {
79 Self {
80 is_file: true,
81 is_dir: false,
82 is_symlink: false,
83 len,
84 }
85 }
86
87 pub fn directory() -> Self {
89 Self {
90 is_file: false,
91 is_dir: true,
92 is_symlink: false,
93 len: 0,
94 }
95 }
96
97 pub fn symlink() -> Self {
99 Self {
100 is_file: false,
101 is_dir: false,
102 is_symlink: true,
103 len: 0,
104 }
105 }
106}
107
108impl From<&Metadata> for FileMetadata {
109 fn from(meta: &Metadata) -> Self {
110 Self {
111 is_file: meta.is_file(),
112 is_dir: meta.is_dir(),
113 is_symlink: meta.file_type().is_symlink(),
114 len: meta.len(),
115 }
116 }
117}
118
119#[derive(Debug, Clone)]
121pub struct DirEntry {
122 pub path: PathBuf,
124 pub metadata: FileMetadata,
126}
127
128pub trait FileSystem: Send + Sync + std::fmt::Debug {
133 fn exists(&self, path: &Path) -> bool;
135
136 fn is_file(&self, path: &Path) -> bool;
138
139 fn is_dir(&self, path: &Path) -> bool;
141
142 fn is_symlink(&self, path: &Path) -> bool;
144
145 fn metadata(&self, path: &Path) -> io::Result<FileMetadata>;
147
148 fn symlink_metadata(&self, path: &Path) -> io::Result<FileMetadata>;
150
151 fn read_to_string(&self, path: &Path) -> LintResult<String>;
153
154 fn write(&self, path: &Path, content: &str) -> LintResult<()>;
156
157 fn canonicalize(&self, path: &Path) -> io::Result<PathBuf>;
159
160 fn read_dir(&self, path: &Path) -> io::Result<Vec<DirEntry>>;
162}
163
164#[derive(Debug, Clone, Copy, Default)]
166pub struct RealFileSystem;
167
168impl FileSystem for RealFileSystem {
169 fn exists(&self, path: &Path) -> bool {
170 path.exists()
171 }
172
173 fn is_file(&self, path: &Path) -> bool {
174 path.is_file()
175 }
176
177 fn is_dir(&self, path: &Path) -> bool {
178 path.is_dir()
179 }
180
181 fn is_symlink(&self, path: &Path) -> bool {
182 path.is_symlink()
183 }
184
185 fn metadata(&self, path: &Path) -> io::Result<FileMetadata> {
186 std::fs::metadata(path).map(|m| FileMetadata::from(&m))
187 }
188
189 fn symlink_metadata(&self, path: &Path) -> io::Result<FileMetadata> {
190 std::fs::symlink_metadata(path).map(|m| FileMetadata::from(&m))
191 }
192
193 fn read_to_string(&self, path: &Path) -> LintResult<String> {
194 crate::file_utils::safe_read_file(path)
195 }
196
197 fn write(&self, path: &Path, content: &str) -> LintResult<()> {
198 crate::file_utils::safe_write_file(path, content)
199 }
200
201 fn canonicalize(&self, path: &Path) -> io::Result<PathBuf> {
202 std::fs::canonicalize(path)
203 }
204
205 fn read_dir(&self, path: &Path) -> io::Result<Vec<DirEntry>> {
206 Ok(std::fs::read_dir(path)?
207 .filter_map(|entry_res| {
208 let entry = entry_res.ok()?;
211 let path = entry.path();
212 let metadata = std::fs::symlink_metadata(&path).ok()?;
215 Some(DirEntry {
216 path,
217 metadata: FileMetadata::from(&metadata),
218 })
219 })
220 .collect())
221 }
222}
223
224#[derive(Debug, Clone)]
226enum MockEntry {
227 File { content: String },
228 Directory,
229 Symlink { target: PathBuf },
230}
231
232#[derive(Debug, Default)]
237pub struct MockFileSystem {
238 entries: RwLock<HashMap<PathBuf, MockEntry>>,
239}
240
241impl MockFileSystem {
242 pub fn new() -> Self {
244 Self {
245 entries: RwLock::new(HashMap::new()),
246 }
247 }
248
249 pub fn add_file(&self, path: impl AsRef<Path>, content: impl Into<String>) {
251 let path = normalize_mock_path(path.as_ref());
252 let mut entries = self.entries.write().expect("MockFileSystem lock poisoned");
253 entries.insert(
254 path,
255 MockEntry::File {
256 content: content.into(),
257 },
258 );
259 }
260
261 pub fn add_dir(&self, path: impl AsRef<Path>) {
263 let path = normalize_mock_path(path.as_ref());
264 let mut entries = self.entries.write().expect("MockFileSystem lock poisoned");
265 entries.insert(path, MockEntry::Directory);
266 }
267
268 pub fn add_symlink(&self, path: impl AsRef<Path>, target: impl AsRef<Path>) {
270 let path = normalize_mock_path(path.as_ref());
271 let target = normalize_mock_path(target.as_ref());
272 let mut entries = self.entries.write().expect("MockFileSystem lock poisoned");
273 entries.insert(path, MockEntry::Symlink { target });
274 }
275
276 pub fn remove(&self, path: impl AsRef<Path>) {
278 let path = normalize_mock_path(path.as_ref());
279 let mut entries = self.entries.write().expect("MockFileSystem lock poisoned");
280 entries.remove(&path);
281 }
282
283 pub fn clear(&self) {
285 let mut entries = self.entries.write().expect("MockFileSystem lock poisoned");
286 entries.clear();
287 }
288
289 fn get_entry(&self, path: &Path) -> Option<MockEntry> {
290 let path = normalize_mock_path(path);
291 let entries = self.entries.read().expect("MockFileSystem lock poisoned");
292 entries.get(&path).cloned()
293 }
294
295 fn resolve_symlink(&self, path: &Path) -> Option<PathBuf> {
296 let path = normalize_mock_path(path);
297 let entries = self.entries.read().expect("MockFileSystem lock poisoned");
298 match entries.get(&path) {
299 Some(MockEntry::Symlink { target }) => Some(target.clone()),
300 _ => None,
301 }
302 }
303
304 pub const MAX_SYMLINK_DEPTH: u32 = 40;
310
311 fn metadata_with_depth(&self, path: &Path, depth: u32) -> io::Result<FileMetadata> {
313 if depth > Self::MAX_SYMLINK_DEPTH {
314 return Err(io::Error::other("too many levels of symbolic links"));
315 }
316
317 enum MetaResult {
319 Found(FileMetadata),
320 FollowSymlink(PathBuf),
321 }
322
323 let path = normalize_mock_path(path);
324
325 let result: io::Result<MetaResult> = {
326 let entries = self.entries.read().expect("MockFileSystem lock poisoned");
327 match entries.get(&path) {
328 None => Err(io::Error::new(
329 io::ErrorKind::NotFound,
330 format!("path not found: {}", path.display()),
331 )),
332 Some(MockEntry::File { content }) => {
333 Ok(MetaResult::Found(FileMetadata::file(content.len() as u64)))
334 }
335 Some(MockEntry::Directory) => Ok(MetaResult::Found(FileMetadata::directory())),
336 Some(MockEntry::Symlink { target }) => {
337 Ok(MetaResult::FollowSymlink(target.clone()))
338 }
339 }
340 };
341
342 match result? {
343 MetaResult::Found(meta) => Ok(meta),
344 MetaResult::FollowSymlink(target) => self.metadata_with_depth(&target, depth + 1),
345 }
346 }
347
348 fn canonicalize_with_depth(&self, path: &Path, depth: u32) -> io::Result<PathBuf> {
350 if depth > Self::MAX_SYMLINK_DEPTH {
351 return Err(io::Error::other("too many levels of symbolic links"));
352 }
353
354 let path_normalized = normalize_mock_path(path);
355
356 if !self.exists(&path_normalized) {
357 return Err(io::Error::new(
358 io::ErrorKind::NotFound,
359 format!("path not found: {}", path.display()),
360 ));
361 }
362
363 if let Some(target) = self.resolve_symlink(&path_normalized) {
365 self.canonicalize_with_depth(&target, depth + 1)
366 } else {
367 Ok(path_normalized)
368 }
369 }
370}
371
372fn normalize_mock_path(path: &Path) -> PathBuf {
375 let path_str = path.to_string_lossy();
376 PathBuf::from(path_str.replace('\\', "/"))
377}
378
379impl FileSystem for MockFileSystem {
380 fn exists(&self, path: &Path) -> bool {
381 self.get_entry(path).is_some()
382 }
383
384 fn is_file(&self, path: &Path) -> bool {
385 matches!(self.get_entry(path), Some(MockEntry::File { .. }))
386 }
387
388 fn is_dir(&self, path: &Path) -> bool {
389 matches!(self.get_entry(path), Some(MockEntry::Directory))
390 }
391
392 fn is_symlink(&self, path: &Path) -> bool {
393 matches!(self.get_entry(path), Some(MockEntry::Symlink { .. }))
394 }
395
396 fn metadata(&self, path: &Path) -> io::Result<FileMetadata> {
397 self.metadata_with_depth(path, 0)
398 }
399
400 fn symlink_metadata(&self, path: &Path) -> io::Result<FileMetadata> {
401 let entry = self.get_entry(path).ok_or_else(|| {
403 io::Error::new(
404 io::ErrorKind::NotFound,
405 format!("path not found: {}", path.display()),
406 )
407 })?;
408
409 match entry {
410 MockEntry::File { content } => Ok(FileMetadata::file(content.len() as u64)),
411 MockEntry::Directory => Ok(FileMetadata::directory()),
412 MockEntry::Symlink { .. } => Ok(FileMetadata::symlink()),
413 }
414 }
415
416 fn read_to_string(&self, path: &Path) -> LintResult<String> {
417 let path_normalized = normalize_mock_path(path);
418 let entries = self.entries.read().expect("MockFileSystem lock poisoned");
419
420 let entry = entries.get(&path_normalized).ok_or_else(|| {
421 CoreError::File(FileError::Read {
422 path: path.to_path_buf(),
423 source: io::Error::new(io::ErrorKind::NotFound, "file not found"),
424 })
425 })?;
426
427 match entry {
428 MockEntry::File { content } => Ok(content.clone()),
429 MockEntry::Directory => Err(CoreError::File(FileError::NotRegular {
430 path: path.to_path_buf(),
431 })),
432 MockEntry::Symlink { target } => {
433 let target_entry = entries.get(target).ok_or_else(|| {
435 CoreError::File(FileError::Read {
436 path: path.to_path_buf(),
437 source: io::Error::new(io::ErrorKind::NotFound, "symlink target not found"),
438 })
439 })?;
440 match target_entry {
441 MockEntry::File { content } => Ok(content.clone()),
442 _ => Err(CoreError::File(FileError::NotRegular {
443 path: path.to_path_buf(),
444 })),
445 }
446 }
447 }
448 }
449
450 fn write(&self, path: &Path, content: &str) -> LintResult<()> {
451 let path_normalized = normalize_mock_path(path);
452 let mut entries = self.entries.write().expect("MockFileSystem lock poisoned");
453
454 match entries.get(&path_normalized) {
456 Some(MockEntry::File { .. }) => {
457 entries.insert(
459 path_normalized,
460 MockEntry::File {
461 content: content.to_string(),
462 },
463 );
464 Ok(())
465 }
466 Some(MockEntry::Directory) => Err(CoreError::File(FileError::NotRegular {
467 path: path.to_path_buf(),
468 })),
469 Some(MockEntry::Symlink { .. }) => Err(CoreError::File(FileError::Symlink {
470 path: path.to_path_buf(),
471 })),
472 None => {
473 Err(CoreError::File(FileError::Write {
475 path: path.to_path_buf(),
476 source: io::Error::new(io::ErrorKind::NotFound, "file not found"),
477 }))
478 }
479 }
480 }
481
482 fn canonicalize(&self, path: &Path) -> io::Result<PathBuf> {
483 self.canonicalize_with_depth(path, 0)
484 }
485
486 fn read_dir(&self, path: &Path) -> io::Result<Vec<DirEntry>> {
487 let path_normalized = normalize_mock_path(path);
488
489 match self.get_entry(&path_normalized) {
491 Some(MockEntry::Directory) => {}
492 Some(_) => {
493 return Err(io::Error::new(
494 io::ErrorKind::NotADirectory,
495 "not a directory",
496 ));
497 }
498 None => {
499 return Err(io::Error::new(
500 io::ErrorKind::NotFound,
501 "directory not found",
502 ));
503 }
504 }
505
506 let entries = self.entries.read().expect("MockFileSystem lock poisoned");
507 let mut result = Vec::new();
508
509 let prefix = if path_normalized.to_string_lossy().ends_with('/') {
511 path_normalized.to_string_lossy().to_string()
512 } else {
513 format!("{}/", path_normalized.display())
514 };
515
516 for (entry_path, entry) in entries.iter() {
517 let entry_str = entry_path.to_string_lossy();
518
519 if let Some(rest) = entry_str.strip_prefix(&prefix) {
521 if !rest.contains('/') && !rest.is_empty() {
523 let metadata = match entry {
524 MockEntry::File { content } => FileMetadata::file(content.len() as u64),
525 MockEntry::Directory => FileMetadata::directory(),
526 MockEntry::Symlink { .. } => FileMetadata::symlink(),
527 };
528 result.push(DirEntry {
529 path: entry_path.clone(),
530 metadata,
531 });
532 }
533 }
534 }
535
536 Ok(result)
537 }
538}
539
540#[cfg(test)]
541mod tests {
542 use super::*;
543
544 #[test]
547 fn test_real_fs_exists() {
548 let fs = RealFileSystem;
549 assert!(fs.exists(Path::new("Cargo.toml")));
551 assert!(!fs.exists(Path::new("nonexistent_file_xyz.txt")));
552 }
553
554 #[test]
555 fn test_real_fs_is_file() {
556 let fs = RealFileSystem;
557 assert!(fs.is_file(Path::new("Cargo.toml")));
558 assert!(!fs.is_file(Path::new("src")));
559 }
560
561 #[test]
562 fn test_real_fs_is_dir() {
563 let fs = RealFileSystem;
564 assert!(fs.is_dir(Path::new("src")));
565 assert!(!fs.is_dir(Path::new("Cargo.toml")));
566 }
567
568 #[test]
569 fn test_real_fs_read_to_string() {
570 let fs = RealFileSystem;
571 let content = fs.read_to_string(Path::new("Cargo.toml"));
572 assert!(content.is_ok());
573 assert!(content.unwrap().contains("[package]"));
574 }
575
576 #[test]
577 fn test_real_fs_read_nonexistent() {
578 let fs = RealFileSystem;
579 let result = fs.read_to_string(Path::new("nonexistent_file_xyz.txt"));
580 assert!(result.is_err());
581 }
582
583 #[test]
586 fn test_mock_fs_add_and_exists() {
587 let fs = MockFileSystem::new();
588 assert!(!fs.exists(Path::new("/test/file.txt")));
589
590 fs.add_file("/test/file.txt", "content");
591 assert!(fs.exists(Path::new("/test/file.txt")));
592 }
593
594 #[test]
595 fn test_mock_fs_is_file() {
596 let fs = MockFileSystem::new();
597 fs.add_file("/test/file.txt", "content");
598 fs.add_dir("/test/dir");
599
600 assert!(fs.is_file(Path::new("/test/file.txt")));
601 assert!(!fs.is_file(Path::new("/test/dir")));
602 }
603
604 #[test]
605 fn test_mock_fs_is_dir() {
606 let fs = MockFileSystem::new();
607 fs.add_file("/test/file.txt", "content");
608 fs.add_dir("/test/dir");
609
610 assert!(!fs.is_dir(Path::new("/test/file.txt")));
611 assert!(fs.is_dir(Path::new("/test/dir")));
612 }
613
614 #[test]
615 fn test_mock_fs_is_symlink() {
616 let fs = MockFileSystem::new();
617 fs.add_file("/test/file.txt", "content");
618 fs.add_symlink("/test/link.txt", "/test/file.txt");
619
620 assert!(!fs.is_symlink(Path::new("/test/file.txt")));
621 assert!(fs.is_symlink(Path::new("/test/link.txt")));
622 }
623
624 #[test]
625 fn test_mock_fs_read_to_string() {
626 let fs = MockFileSystem::new();
627 fs.add_file("/test/file.txt", "hello world");
628
629 let content = fs.read_to_string(Path::new("/test/file.txt"));
630 assert!(content.is_ok());
631 assert_eq!(content.unwrap(), "hello world");
632 }
633
634 #[test]
635 fn test_mock_fs_read_nonexistent() {
636 let fs = MockFileSystem::new();
637 let result = fs.read_to_string(Path::new("/test/file.txt"));
638 assert!(result.is_err());
639 }
640
641 #[test]
642 fn test_mock_fs_read_directory_fails() {
643 let fs = MockFileSystem::new();
644 fs.add_dir("/test/dir");
645
646 let result = fs.read_to_string(Path::new("/test/dir"));
647 assert!(matches!(
648 result,
649 Err(CoreError::File(FileError::NotRegular { .. }))
650 ));
651 }
652
653 #[test]
654 fn test_mock_fs_read_symlink_follows_target() {
655 let fs = MockFileSystem::new();
656 fs.add_file("/test/file.txt", "content");
657 fs.add_symlink("/test/link.txt", "/test/file.txt");
658
659 let result = fs.read_to_string(Path::new("/test/link.txt"));
660 assert!(result.is_ok());
661 assert_eq!(result.unwrap(), "content");
662 }
663
664 #[test]
665 fn test_mock_fs_write() {
666 let fs = MockFileSystem::new();
667 fs.add_file("/test/file.txt", "original");
668
669 let result = fs.write(Path::new("/test/file.txt"), "updated");
670 assert!(result.is_ok());
671
672 let content = fs.read_to_string(Path::new("/test/file.txt")).unwrap();
673 assert_eq!(content, "updated");
674 }
675
676 #[test]
677 fn test_mock_fs_write_nonexistent_fails() {
678 let fs = MockFileSystem::new();
679
680 let result = fs.write(Path::new("/test/file.txt"), "content");
681 assert!(matches!(
682 result,
683 Err(CoreError::File(FileError::Write { .. }))
684 ));
685 }
686
687 #[test]
688 fn test_mock_fs_metadata_file() {
689 let fs = MockFileSystem::new();
690 fs.add_file("/test/file.txt", "12345");
691
692 let meta = fs.metadata(Path::new("/test/file.txt")).unwrap();
693 assert!(meta.is_file);
694 assert!(!meta.is_dir);
695 assert!(!meta.is_symlink);
696 assert_eq!(meta.len, 5);
697 }
698
699 #[test]
700 fn test_mock_fs_metadata_directory() {
701 let fs = MockFileSystem::new();
702 fs.add_dir("/test/dir");
703
704 let meta = fs.metadata(Path::new("/test/dir")).unwrap();
705 assert!(!meta.is_file);
706 assert!(meta.is_dir);
707 assert!(!meta.is_symlink);
708 }
709
710 #[test]
711 fn test_mock_fs_symlink_metadata() {
712 let fs = MockFileSystem::new();
713 fs.add_file("/test/file.txt", "content");
714 fs.add_symlink("/test/link.txt", "/test/file.txt");
715
716 let meta = fs.symlink_metadata(Path::new("/test/link.txt")).unwrap();
718 assert!(meta.is_symlink);
719
720 let meta = fs.metadata(Path::new("/test/link.txt")).unwrap();
722 assert!(meta.is_file);
723 assert!(!meta.is_symlink);
724 }
725
726 #[test]
727 fn test_mock_fs_read_dir() {
728 let fs = MockFileSystem::new();
729 fs.add_dir("/test");
730 fs.add_file("/test/file1.txt", "content1");
731 fs.add_file("/test/file2.txt", "content2");
732 fs.add_dir("/test/subdir");
733
734 let entries = fs.read_dir(Path::new("/test")).unwrap();
735 assert_eq!(entries.len(), 3);
736
737 let names: Vec<_> = entries
738 .iter()
739 .map(|e| e.path.file_name().unwrap().to_string_lossy().to_string())
740 .collect();
741 assert!(names.contains(&"file1.txt".to_string()));
742 assert!(names.contains(&"file2.txt".to_string()));
743 assert!(names.contains(&"subdir".to_string()));
744 }
745
746 #[test]
747 fn test_mock_fs_read_dir_nonexistent() {
748 let fs = MockFileSystem::new();
749 let result = fs.read_dir(Path::new("/nonexistent"));
750 assert!(result.is_err());
751 }
752
753 #[test]
754 fn test_mock_fs_read_dir_not_directory() {
755 let fs = MockFileSystem::new();
756 fs.add_file("/test/file.txt", "content");
757
758 let result = fs.read_dir(Path::new("/test/file.txt"));
759 assert!(result.is_err());
760 }
761
762 #[test]
763 fn test_mock_fs_canonicalize() {
764 let fs = MockFileSystem::new();
765 fs.add_file("/test/file.txt", "content");
766
767 let canonical = fs.canonicalize(Path::new("/test/file.txt")).unwrap();
768 assert_eq!(canonical, PathBuf::from("/test/file.txt"));
769 }
770
771 #[test]
772 fn test_mock_fs_canonicalize_follows_symlink() {
773 let fs = MockFileSystem::new();
774 fs.add_file("/test/file.txt", "content");
775 fs.add_symlink("/test/link.txt", "/test/file.txt");
776
777 let canonical = fs.canonicalize(Path::new("/test/link.txt")).unwrap();
778 assert_eq!(canonical, PathBuf::from("/test/file.txt"));
779 }
780
781 #[test]
782 fn test_mock_fs_clear() {
783 let fs = MockFileSystem::new();
784 fs.add_file("/test/file.txt", "content");
785 assert!(fs.exists(Path::new("/test/file.txt")));
786
787 fs.clear();
788 assert!(!fs.exists(Path::new("/test/file.txt")));
789 }
790
791 #[test]
792 fn test_mock_fs_remove() {
793 let fs = MockFileSystem::new();
794 fs.add_file("/test/file.txt", "content");
795 assert!(fs.exists(Path::new("/test/file.txt")));
796
797 fs.remove("/test/file.txt");
798 assert!(!fs.exists(Path::new("/test/file.txt")));
799 }
800
801 #[test]
802 fn test_mock_fs_windows_path_normalization() {
803 let fs = MockFileSystem::new();
804 fs.add_file("C:/test/file.txt", "content");
805
806 assert!(fs.exists(Path::new("C:/test/file.txt")));
808 assert!(fs.exists(Path::new("C:\\test\\file.txt")));
809 }
810
811 #[test]
812 fn test_mock_fs_thread_safety() {
813 use std::sync::Arc;
814 use std::thread;
815
816 let fs = Arc::new(MockFileSystem::new());
817 let mut handles = vec![];
818
819 for i in 0..10 {
821 let fs_clone = Arc::clone(&fs);
822 let handle = thread::spawn(move || {
823 let path = format!("/test/file{}.txt", i);
824 fs_clone.add_file(&path, format!("content{}", i));
825 assert!(fs_clone.exists(Path::new(&path)));
826 let content = fs_clone.read_to_string(Path::new(&path)).unwrap();
827 assert_eq!(content, format!("content{}", i));
828 });
829 handles.push(handle);
830 }
831
832 for handle in handles {
833 handle.join().unwrap();
834 }
835
836 for i in 0..10 {
838 let path = format!("/test/file{}.txt", i);
839 assert!(fs.exists(Path::new(&path)));
840 }
841 }
842
843 #[test]
844 fn test_mock_fs_circular_symlink_metadata() {
845 let fs = MockFileSystem::new();
846 fs.add_symlink("/test/a", "/test/b");
848 fs.add_symlink("/test/b", "/test/a");
849
850 let result = fs.metadata(Path::new("/test/a"));
852 assert!(result.is_err());
853 assert!(
854 result
855 .unwrap_err()
856 .to_string()
857 .contains("too many levels of symbolic links")
858 );
859 }
860
861 #[test]
862 fn test_mock_fs_circular_symlink_canonicalize() {
863 let fs = MockFileSystem::new();
864 fs.add_symlink("/test/a", "/test/b");
866 fs.add_symlink("/test/b", "/test/a");
867
868 let result = fs.canonicalize(Path::new("/test/a"));
870 assert!(result.is_err());
871 assert!(
872 result
873 .unwrap_err()
874 .to_string()
875 .contains("too many levels of symbolic links")
876 );
877 }
878
879 #[test]
880 fn test_mock_fs_chained_symlinks() {
881 let fs = MockFileSystem::new();
882 fs.add_file("/test/file.txt", "content");
884 fs.add_symlink("/test/link3", "/test/file.txt");
885 fs.add_symlink("/test/link2", "/test/link3");
886 fs.add_symlink("/test/link1", "/test/link2");
887
888 let meta = fs.metadata(Path::new("/test/link1")).unwrap();
890 assert!(meta.is_file);
891 assert_eq!(meta.len, 7); let canonical = fs.canonicalize(Path::new("/test/link1")).unwrap();
895 assert_eq!(canonical, PathBuf::from("/test/file.txt"));
896 }
897
898 #[test]
899 fn test_mock_fs_max_symlink_depth_boundary() {
900 let fs = MockFileSystem::new();
902 fs.add_file("/test/target.txt", "content");
903
904 let mut prev = PathBuf::from("/test/target.txt");
906 for i in 0..MockFileSystem::MAX_SYMLINK_DEPTH {
907 let link = PathBuf::from(format!("/test/link{}", i));
908 fs.add_symlink(&link, &prev);
909 prev = link;
910 }
911
912 let result = fs.metadata(&prev);
914 assert!(result.is_ok(), "Should handle MAX_SYMLINK_DEPTH links");
915 }
916
917 #[test]
918 fn test_mock_fs_exceeds_max_symlink_depth() {
919 let fs = MockFileSystem::new();
921 fs.add_file("/test/target.txt", "content");
922
923 let mut prev = PathBuf::from("/test/target.txt");
925 for i in 0..=MockFileSystem::MAX_SYMLINK_DEPTH {
926 let link = PathBuf::from(format!("/test/link{}", i));
927 fs.add_symlink(&link, &prev);
928 prev = link;
929 }
930
931 let result = fs.metadata(&prev);
933 assert!(
934 result.is_err(),
935 "Should fail when exceeding MAX_SYMLINK_DEPTH"
936 );
937 assert!(
938 result
939 .unwrap_err()
940 .to_string()
941 .contains("too many levels of symbolic links")
942 );
943 }
944
945 #[cfg(unix)]
948 mod unix_tests {
949 use super::*;
950 use std::os::unix::fs::symlink;
951 use tempfile::TempDir;
952
953 #[test]
954 fn test_real_fs_follows_symlink_read() {
955 let temp = TempDir::new().unwrap();
956 let target = temp.path().join("target.txt");
957 let link = temp.path().join("link.txt");
958
959 std::fs::write(&target, "content").unwrap();
960 symlink(&target, &link).unwrap();
961
962 let fs = RealFileSystem;
963 let result = fs.read_to_string(&link);
964
965 assert!(result.is_ok());
966 assert_eq!(result.unwrap(), "content");
967 }
968
969 #[test]
970 fn test_real_fs_symlink_metadata() {
971 let temp = TempDir::new().unwrap();
972 let target = temp.path().join("target.txt");
973 let link = temp.path().join("link.txt");
974
975 std::fs::write(&target, "content").unwrap();
976 symlink(&target, &link).unwrap();
977
978 let fs = RealFileSystem;
979
980 let meta = fs.symlink_metadata(&link).unwrap();
982 assert!(meta.is_symlink);
983
984 let meta = fs.metadata(&link).unwrap();
986 assert!(meta.is_file);
987 assert!(!meta.is_symlink);
988 }
989
990 #[test]
991 fn test_real_fs_dangling_symlink() {
992 let temp = TempDir::new().unwrap();
993 let link = temp.path().join("dangling.txt");
994
995 symlink("/nonexistent/target", &link).unwrap();
996
997 let fs = RealFileSystem;
998 let result = fs.read_to_string(&link);
999
1000 assert!(result.is_err());
1002 assert!(matches!(
1003 result.unwrap_err(),
1004 CoreError::File(FileError::Read { .. })
1005 ));
1006 }
1007
1008 #[test]
1009 fn test_real_fs_is_symlink() {
1010 let temp = TempDir::new().unwrap();
1011 let target = temp.path().join("target.txt");
1012 let link = temp.path().join("link.txt");
1013
1014 std::fs::write(&target, "content").unwrap();
1015 symlink(&target, &link).unwrap();
1016
1017 let fs = RealFileSystem;
1018
1019 assert!(!fs.is_symlink(&target));
1020 assert!(fs.is_symlink(&link));
1021 }
1022
1023 #[test]
1024 fn test_real_fs_read_dir_skips_symlinks_in_metadata() {
1025 let temp = TempDir::new().unwrap();
1026
1027 std::fs::write(temp.path().join("file.txt"), "content").unwrap();
1029
1030 symlink(temp.path().join("file.txt"), temp.path().join("link.txt")).unwrap();
1032
1033 let fs = RealFileSystem;
1034 let entries = fs.read_dir(temp.path()).unwrap();
1035
1036 assert_eq!(entries.len(), 2);
1038
1039 let symlink_entry = entries
1041 .iter()
1042 .find(|e| e.path.file_name().unwrap().to_str().unwrap() == "link.txt");
1043 assert!(symlink_entry.is_some());
1044 assert!(symlink_entry.unwrap().metadata.is_symlink);
1045
1046 let file_entry = entries
1048 .iter()
1049 .find(|e| e.path.file_name().unwrap().to_str().unwrap() == "file.txt");
1050 assert!(file_entry.is_some());
1051 assert!(file_entry.unwrap().metadata.is_file);
1052 }
1053 }
1054}