1use crate::diagnostics::{LintError, 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
421 .get(&path_normalized)
422 .ok_or_else(|| LintError::FileRead {
423 path: path.to_path_buf(),
424 source: io::Error::new(io::ErrorKind::NotFound, "file not found"),
425 })?;
426
427 match entry {
428 MockEntry::File { content } => Ok(content.clone()),
429 MockEntry::Directory => Err(LintError::FileNotRegular {
430 path: path.to_path_buf(),
431 }),
432 MockEntry::Symlink { .. } => Err(LintError::FileSymlink {
433 path: path.to_path_buf(),
434 }),
435 }
436 }
437
438 fn write(&self, path: &Path, content: &str) -> LintResult<()> {
439 let path_normalized = normalize_mock_path(path);
440 let mut entries = self.entries.write().expect("MockFileSystem lock poisoned");
441
442 match entries.get(&path_normalized) {
444 Some(MockEntry::File { .. }) => {
445 entries.insert(
447 path_normalized,
448 MockEntry::File {
449 content: content.to_string(),
450 },
451 );
452 Ok(())
453 }
454 Some(MockEntry::Directory) => Err(LintError::FileNotRegular {
455 path: path.to_path_buf(),
456 }),
457 Some(MockEntry::Symlink { .. }) => Err(LintError::FileSymlink {
458 path: path.to_path_buf(),
459 }),
460 None => {
461 Err(LintError::FileWrite {
463 path: path.to_path_buf(),
464 source: io::Error::new(io::ErrorKind::NotFound, "file not found"),
465 })
466 }
467 }
468 }
469
470 fn canonicalize(&self, path: &Path) -> io::Result<PathBuf> {
471 self.canonicalize_with_depth(path, 0)
472 }
473
474 fn read_dir(&self, path: &Path) -> io::Result<Vec<DirEntry>> {
475 let path_normalized = normalize_mock_path(path);
476
477 match self.get_entry(&path_normalized) {
479 Some(MockEntry::Directory) => {}
480 Some(_) => {
481 return Err(io::Error::new(
482 io::ErrorKind::NotADirectory,
483 "not a directory",
484 ));
485 }
486 None => {
487 return Err(io::Error::new(
488 io::ErrorKind::NotFound,
489 "directory not found",
490 ));
491 }
492 }
493
494 let entries = self.entries.read().expect("MockFileSystem lock poisoned");
495 let mut result = Vec::new();
496
497 let prefix = if path_normalized.to_string_lossy().ends_with('/') {
499 path_normalized.to_string_lossy().to_string()
500 } else {
501 format!("{}/", path_normalized.display())
502 };
503
504 for (entry_path, entry) in entries.iter() {
505 let entry_str = entry_path.to_string_lossy();
506
507 if let Some(rest) = entry_str.strip_prefix(&prefix) {
509 if !rest.contains('/') && !rest.is_empty() {
511 let metadata = match entry {
512 MockEntry::File { content } => FileMetadata::file(content.len() as u64),
513 MockEntry::Directory => FileMetadata::directory(),
514 MockEntry::Symlink { .. } => FileMetadata::symlink(),
515 };
516 result.push(DirEntry {
517 path: entry_path.clone(),
518 metadata,
519 });
520 }
521 }
522 }
523
524 Ok(result)
525 }
526}
527
528#[cfg(test)]
529mod tests {
530 use super::*;
531
532 #[test]
535 fn test_real_fs_exists() {
536 let fs = RealFileSystem;
537 assert!(fs.exists(Path::new("Cargo.toml")));
539 assert!(!fs.exists(Path::new("nonexistent_file_xyz.txt")));
540 }
541
542 #[test]
543 fn test_real_fs_is_file() {
544 let fs = RealFileSystem;
545 assert!(fs.is_file(Path::new("Cargo.toml")));
546 assert!(!fs.is_file(Path::new("src")));
547 }
548
549 #[test]
550 fn test_real_fs_is_dir() {
551 let fs = RealFileSystem;
552 assert!(fs.is_dir(Path::new("src")));
553 assert!(!fs.is_dir(Path::new("Cargo.toml")));
554 }
555
556 #[test]
557 fn test_real_fs_read_to_string() {
558 let fs = RealFileSystem;
559 let content = fs.read_to_string(Path::new("Cargo.toml"));
560 assert!(content.is_ok());
561 assert!(content.unwrap().contains("[package]"));
562 }
563
564 #[test]
565 fn test_real_fs_read_nonexistent() {
566 let fs = RealFileSystem;
567 let result = fs.read_to_string(Path::new("nonexistent_file_xyz.txt"));
568 assert!(result.is_err());
569 }
570
571 #[test]
574 fn test_mock_fs_add_and_exists() {
575 let fs = MockFileSystem::new();
576 assert!(!fs.exists(Path::new("/test/file.txt")));
577
578 fs.add_file("/test/file.txt", "content");
579 assert!(fs.exists(Path::new("/test/file.txt")));
580 }
581
582 #[test]
583 fn test_mock_fs_is_file() {
584 let fs = MockFileSystem::new();
585 fs.add_file("/test/file.txt", "content");
586 fs.add_dir("/test/dir");
587
588 assert!(fs.is_file(Path::new("/test/file.txt")));
589 assert!(!fs.is_file(Path::new("/test/dir")));
590 }
591
592 #[test]
593 fn test_mock_fs_is_dir() {
594 let fs = MockFileSystem::new();
595 fs.add_file("/test/file.txt", "content");
596 fs.add_dir("/test/dir");
597
598 assert!(!fs.is_dir(Path::new("/test/file.txt")));
599 assert!(fs.is_dir(Path::new("/test/dir")));
600 }
601
602 #[test]
603 fn test_mock_fs_is_symlink() {
604 let fs = MockFileSystem::new();
605 fs.add_file("/test/file.txt", "content");
606 fs.add_symlink("/test/link.txt", "/test/file.txt");
607
608 assert!(!fs.is_symlink(Path::new("/test/file.txt")));
609 assert!(fs.is_symlink(Path::new("/test/link.txt")));
610 }
611
612 #[test]
613 fn test_mock_fs_read_to_string() {
614 let fs = MockFileSystem::new();
615 fs.add_file("/test/file.txt", "hello world");
616
617 let content = fs.read_to_string(Path::new("/test/file.txt"));
618 assert!(content.is_ok());
619 assert_eq!(content.unwrap(), "hello world");
620 }
621
622 #[test]
623 fn test_mock_fs_read_nonexistent() {
624 let fs = MockFileSystem::new();
625 let result = fs.read_to_string(Path::new("/test/file.txt"));
626 assert!(result.is_err());
627 }
628
629 #[test]
630 fn test_mock_fs_read_directory_fails() {
631 let fs = MockFileSystem::new();
632 fs.add_dir("/test/dir");
633
634 let result = fs.read_to_string(Path::new("/test/dir"));
635 assert!(matches!(result, Err(LintError::FileNotRegular { .. })));
636 }
637
638 #[test]
639 fn test_mock_fs_read_symlink_fails() {
640 let fs = MockFileSystem::new();
641 fs.add_file("/test/file.txt", "content");
642 fs.add_symlink("/test/link.txt", "/test/file.txt");
643
644 let result = fs.read_to_string(Path::new("/test/link.txt"));
645 assert!(matches!(result, Err(LintError::FileSymlink { .. })));
646 }
647
648 #[test]
649 fn test_mock_fs_write() {
650 let fs = MockFileSystem::new();
651 fs.add_file("/test/file.txt", "original");
652
653 let result = fs.write(Path::new("/test/file.txt"), "updated");
654 assert!(result.is_ok());
655
656 let content = fs.read_to_string(Path::new("/test/file.txt")).unwrap();
657 assert_eq!(content, "updated");
658 }
659
660 #[test]
661 fn test_mock_fs_write_nonexistent_fails() {
662 let fs = MockFileSystem::new();
663
664 let result = fs.write(Path::new("/test/file.txt"), "content");
665 assert!(matches!(result, Err(LintError::FileWrite { .. })));
666 }
667
668 #[test]
669 fn test_mock_fs_metadata_file() {
670 let fs = MockFileSystem::new();
671 fs.add_file("/test/file.txt", "12345");
672
673 let meta = fs.metadata(Path::new("/test/file.txt")).unwrap();
674 assert!(meta.is_file);
675 assert!(!meta.is_dir);
676 assert!(!meta.is_symlink);
677 assert_eq!(meta.len, 5);
678 }
679
680 #[test]
681 fn test_mock_fs_metadata_directory() {
682 let fs = MockFileSystem::new();
683 fs.add_dir("/test/dir");
684
685 let meta = fs.metadata(Path::new("/test/dir")).unwrap();
686 assert!(!meta.is_file);
687 assert!(meta.is_dir);
688 assert!(!meta.is_symlink);
689 }
690
691 #[test]
692 fn test_mock_fs_symlink_metadata() {
693 let fs = MockFileSystem::new();
694 fs.add_file("/test/file.txt", "content");
695 fs.add_symlink("/test/link.txt", "/test/file.txt");
696
697 let meta = fs.symlink_metadata(Path::new("/test/link.txt")).unwrap();
699 assert!(meta.is_symlink);
700
701 let meta = fs.metadata(Path::new("/test/link.txt")).unwrap();
703 assert!(meta.is_file);
704 assert!(!meta.is_symlink);
705 }
706
707 #[test]
708 fn test_mock_fs_read_dir() {
709 let fs = MockFileSystem::new();
710 fs.add_dir("/test");
711 fs.add_file("/test/file1.txt", "content1");
712 fs.add_file("/test/file2.txt", "content2");
713 fs.add_dir("/test/subdir");
714
715 let entries = fs.read_dir(Path::new("/test")).unwrap();
716 assert_eq!(entries.len(), 3);
717
718 let names: Vec<_> = entries
719 .iter()
720 .map(|e| e.path.file_name().unwrap().to_string_lossy().to_string())
721 .collect();
722 assert!(names.contains(&"file1.txt".to_string()));
723 assert!(names.contains(&"file2.txt".to_string()));
724 assert!(names.contains(&"subdir".to_string()));
725 }
726
727 #[test]
728 fn test_mock_fs_read_dir_nonexistent() {
729 let fs = MockFileSystem::new();
730 let result = fs.read_dir(Path::new("/nonexistent"));
731 assert!(result.is_err());
732 }
733
734 #[test]
735 fn test_mock_fs_read_dir_not_directory() {
736 let fs = MockFileSystem::new();
737 fs.add_file("/test/file.txt", "content");
738
739 let result = fs.read_dir(Path::new("/test/file.txt"));
740 assert!(result.is_err());
741 }
742
743 #[test]
744 fn test_mock_fs_canonicalize() {
745 let fs = MockFileSystem::new();
746 fs.add_file("/test/file.txt", "content");
747
748 let canonical = fs.canonicalize(Path::new("/test/file.txt")).unwrap();
749 assert_eq!(canonical, PathBuf::from("/test/file.txt"));
750 }
751
752 #[test]
753 fn test_mock_fs_canonicalize_follows_symlink() {
754 let fs = MockFileSystem::new();
755 fs.add_file("/test/file.txt", "content");
756 fs.add_symlink("/test/link.txt", "/test/file.txt");
757
758 let canonical = fs.canonicalize(Path::new("/test/link.txt")).unwrap();
759 assert_eq!(canonical, PathBuf::from("/test/file.txt"));
760 }
761
762 #[test]
763 fn test_mock_fs_clear() {
764 let fs = MockFileSystem::new();
765 fs.add_file("/test/file.txt", "content");
766 assert!(fs.exists(Path::new("/test/file.txt")));
767
768 fs.clear();
769 assert!(!fs.exists(Path::new("/test/file.txt")));
770 }
771
772 #[test]
773 fn test_mock_fs_remove() {
774 let fs = MockFileSystem::new();
775 fs.add_file("/test/file.txt", "content");
776 assert!(fs.exists(Path::new("/test/file.txt")));
777
778 fs.remove("/test/file.txt");
779 assert!(!fs.exists(Path::new("/test/file.txt")));
780 }
781
782 #[test]
783 fn test_mock_fs_windows_path_normalization() {
784 let fs = MockFileSystem::new();
785 fs.add_file("C:/test/file.txt", "content");
786
787 assert!(fs.exists(Path::new("C:/test/file.txt")));
789 assert!(fs.exists(Path::new("C:\\test\\file.txt")));
790 }
791
792 #[test]
793 fn test_mock_fs_thread_safety() {
794 use std::sync::Arc;
795 use std::thread;
796
797 let fs = Arc::new(MockFileSystem::new());
798 let mut handles = vec![];
799
800 for i in 0..10 {
802 let fs_clone = Arc::clone(&fs);
803 let handle = thread::spawn(move || {
804 let path = format!("/test/file{}.txt", i);
805 fs_clone.add_file(&path, format!("content{}", i));
806 assert!(fs_clone.exists(Path::new(&path)));
807 let content = fs_clone.read_to_string(Path::new(&path)).unwrap();
808 assert_eq!(content, format!("content{}", i));
809 });
810 handles.push(handle);
811 }
812
813 for handle in handles {
814 handle.join().unwrap();
815 }
816
817 for i in 0..10 {
819 let path = format!("/test/file{}.txt", i);
820 assert!(fs.exists(Path::new(&path)));
821 }
822 }
823
824 #[test]
825 fn test_mock_fs_circular_symlink_metadata() {
826 let fs = MockFileSystem::new();
827 fs.add_symlink("/test/a", "/test/b");
829 fs.add_symlink("/test/b", "/test/a");
830
831 let result = fs.metadata(Path::new("/test/a"));
833 assert!(result.is_err());
834 assert!(
835 result
836 .unwrap_err()
837 .to_string()
838 .contains("too many levels of symbolic links")
839 );
840 }
841
842 #[test]
843 fn test_mock_fs_circular_symlink_canonicalize() {
844 let fs = MockFileSystem::new();
845 fs.add_symlink("/test/a", "/test/b");
847 fs.add_symlink("/test/b", "/test/a");
848
849 let result = fs.canonicalize(Path::new("/test/a"));
851 assert!(result.is_err());
852 assert!(
853 result
854 .unwrap_err()
855 .to_string()
856 .contains("too many levels of symbolic links")
857 );
858 }
859
860 #[test]
861 fn test_mock_fs_chained_symlinks() {
862 let fs = MockFileSystem::new();
863 fs.add_file("/test/file.txt", "content");
865 fs.add_symlink("/test/link3", "/test/file.txt");
866 fs.add_symlink("/test/link2", "/test/link3");
867 fs.add_symlink("/test/link1", "/test/link2");
868
869 let meta = fs.metadata(Path::new("/test/link1")).unwrap();
871 assert!(meta.is_file);
872 assert_eq!(meta.len, 7); let canonical = fs.canonicalize(Path::new("/test/link1")).unwrap();
876 assert_eq!(canonical, PathBuf::from("/test/file.txt"));
877 }
878
879 #[test]
880 fn test_mock_fs_max_symlink_depth_boundary() {
881 let fs = MockFileSystem::new();
883 fs.add_file("/test/target.txt", "content");
884
885 let mut prev = PathBuf::from("/test/target.txt");
887 for i in 0..MockFileSystem::MAX_SYMLINK_DEPTH {
888 let link = PathBuf::from(format!("/test/link{}", i));
889 fs.add_symlink(&link, &prev);
890 prev = link;
891 }
892
893 let result = fs.metadata(&prev);
895 assert!(result.is_ok(), "Should handle MAX_SYMLINK_DEPTH links");
896 }
897
898 #[test]
899 fn test_mock_fs_exceeds_max_symlink_depth() {
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!(
915 result.is_err(),
916 "Should fail when exceeding MAX_SYMLINK_DEPTH"
917 );
918 assert!(
919 result
920 .unwrap_err()
921 .to_string()
922 .contains("too many levels of symbolic links")
923 );
924 }
925
926 #[cfg(unix)]
929 mod unix_tests {
930 use super::*;
931 use std::os::unix::fs::symlink;
932 use tempfile::TempDir;
933
934 #[test]
935 fn test_real_fs_rejects_symlink_read() {
936 let temp = TempDir::new().unwrap();
937 let target = temp.path().join("target.txt");
938 let link = temp.path().join("link.txt");
939
940 std::fs::write(&target, "content").unwrap();
941 symlink(&target, &link).unwrap();
942
943 let fs = RealFileSystem;
944 let result = fs.read_to_string(&link);
945
946 assert!(result.is_err());
947 assert!(matches!(result.unwrap_err(), LintError::FileSymlink { .. }));
948 }
949
950 #[test]
951 fn test_real_fs_symlink_metadata() {
952 let temp = TempDir::new().unwrap();
953 let target = temp.path().join("target.txt");
954 let link = temp.path().join("link.txt");
955
956 std::fs::write(&target, "content").unwrap();
957 symlink(&target, &link).unwrap();
958
959 let fs = RealFileSystem;
960
961 let meta = fs.symlink_metadata(&link).unwrap();
963 assert!(meta.is_symlink);
964
965 let meta = fs.metadata(&link).unwrap();
967 assert!(meta.is_file);
968 assert!(!meta.is_symlink);
969 }
970
971 #[test]
972 fn test_real_fs_dangling_symlink() {
973 let temp = TempDir::new().unwrap();
974 let link = temp.path().join("dangling.txt");
975
976 symlink("/nonexistent/target", &link).unwrap();
977
978 let fs = RealFileSystem;
979 let result = fs.read_to_string(&link);
980
981 assert!(result.is_err());
983 assert!(matches!(result.unwrap_err(), LintError::FileSymlink { .. }));
984 }
985
986 #[test]
987 fn test_real_fs_is_symlink() {
988 let temp = TempDir::new().unwrap();
989 let target = temp.path().join("target.txt");
990 let link = temp.path().join("link.txt");
991
992 std::fs::write(&target, "content").unwrap();
993 symlink(&target, &link).unwrap();
994
995 let fs = RealFileSystem;
996
997 assert!(!fs.is_symlink(&target));
998 assert!(fs.is_symlink(&link));
999 }
1000
1001 #[test]
1002 fn test_real_fs_read_dir_skips_symlinks_in_metadata() {
1003 let temp = TempDir::new().unwrap();
1004
1005 std::fs::write(temp.path().join("file.txt"), "content").unwrap();
1007
1008 symlink(temp.path().join("file.txt"), temp.path().join("link.txt")).unwrap();
1010
1011 let fs = RealFileSystem;
1012 let entries = fs.read_dir(temp.path()).unwrap();
1013
1014 assert_eq!(entries.len(), 2);
1016
1017 let symlink_entry = entries
1019 .iter()
1020 .find(|e| e.path.file_name().unwrap().to_str().unwrap() == "link.txt");
1021 assert!(symlink_entry.is_some());
1022 assert!(symlink_entry.unwrap().metadata.is_symlink);
1023
1024 let file_entry = entries
1026 .iter()
1027 .find(|e| e.path.file_name().unwrap().to_str().unwrap() == "file.txt");
1028 assert!(file_entry.is_some());
1029 assert!(file_entry.unwrap().metadata.is_file);
1030 }
1031 }
1032}