Skip to main content

kaish_kernel/vfs/
memory.rs

1//! In-memory filesystem implementation.
2//!
3//! Used for `/scratch` and testing. All data is ephemeral.
4
5use super::traits::{DirEntry, EntryType, Filesystem, Metadata};
6use async_trait::async_trait;
7use std::collections::HashMap;
8use std::io;
9use std::path::{Path, PathBuf};
10use std::time::SystemTime;
11use tokio::sync::RwLock;
12
13/// Entry in the memory filesystem.
14#[derive(Debug, Clone)]
15enum Entry {
16    File { data: Vec<u8>, modified: SystemTime },
17    Directory { modified: SystemTime },
18    Symlink { target: PathBuf, modified: SystemTime },
19}
20
21/// In-memory filesystem.
22///
23/// Thread-safe via internal `RwLock`. All data is lost when dropped.
24#[derive(Debug)]
25pub struct MemoryFs {
26    entries: RwLock<HashMap<PathBuf, Entry>>,
27}
28
29impl Default for MemoryFs {
30    fn default() -> Self {
31        Self::new()
32    }
33}
34
35impl MemoryFs {
36    /// Create a new empty in-memory filesystem.
37    pub fn new() -> Self {
38        let mut entries = HashMap::new();
39        // Root directory always exists
40        entries.insert(
41            PathBuf::from(""),
42            Entry::Directory {
43                modified: SystemTime::now(),
44            },
45        );
46        Self {
47            entries: RwLock::new(entries),
48        }
49    }
50
51    /// Normalize a path: remove leading `/`, resolve `.` and `..`.
52    fn normalize(path: &Path) -> PathBuf {
53        let mut result = PathBuf::new();
54        for component in path.components() {
55            match component {
56                std::path::Component::RootDir => {}
57                std::path::Component::CurDir => {}
58                std::path::Component::ParentDir => {
59                    result.pop();
60                }
61                std::path::Component::Normal(s) => {
62                    result.push(s);
63                }
64                std::path::Component::Prefix(_) => {}
65            }
66        }
67        result
68    }
69
70    /// Ensure all parent directories exist.
71    async fn ensure_parents(&self, path: &Path) -> io::Result<()> {
72        let mut entries = self.entries.write().await;
73
74        let mut current = PathBuf::new();
75        for component in path.parent().into_iter().flat_map(|p| p.components()) {
76            if let std::path::Component::Normal(s) = component {
77                current.push(s);
78                entries.entry(current.clone()).or_insert(Entry::Directory {
79                    modified: SystemTime::now(),
80                });
81            }
82        }
83        Ok(())
84    }
85}
86
87#[async_trait]
88impl Filesystem for MemoryFs {
89    async fn read(&self, path: &Path) -> io::Result<Vec<u8>> {
90        let normalized = Self::normalize(path);
91        let entries = self.entries.read().await;
92
93        match entries.get(&normalized) {
94            Some(Entry::File { data, .. }) => Ok(data.clone()),
95            Some(Entry::Directory { .. }) => Err(io::Error::new(
96                io::ErrorKind::IsADirectory,
97                format!("is a directory: {}", path.display()),
98            )),
99            Some(Entry::Symlink { target, .. }) => {
100                // Clone target before dropping lock to follow the symlink
101                let target = target.clone();
102                drop(entries);
103                self.read(&target).await
104            }
105            None => Err(io::Error::new(
106                io::ErrorKind::NotFound,
107                format!("not found: {}", path.display()),
108            )),
109        }
110    }
111
112    async fn write(&self, path: &Path, data: &[u8]) -> io::Result<()> {
113        let normalized = Self::normalize(path);
114
115        // Ensure parent directories exist
116        self.ensure_parents(&normalized).await?;
117
118        let mut entries = self.entries.write().await;
119
120        // Check we're not overwriting a directory
121        if let Some(Entry::Directory { .. }) = entries.get(&normalized) {
122            return Err(io::Error::new(
123                io::ErrorKind::IsADirectory,
124                format!("is a directory: {}", path.display()),
125            ));
126        }
127
128        entries.insert(
129            normalized,
130            Entry::File {
131                data: data.to_vec(),
132                modified: SystemTime::now(),
133            },
134        );
135        Ok(())
136    }
137
138    async fn list(&self, path: &Path) -> io::Result<Vec<DirEntry>> {
139        let normalized = Self::normalize(path);
140        let entries = self.entries.read().await;
141
142        // Verify the path is a directory
143        match entries.get(&normalized) {
144            Some(Entry::Directory { .. }) => {}
145            Some(Entry::File { .. }) => {
146                return Err(io::Error::new(
147                    io::ErrorKind::NotADirectory,
148                    format!("not a directory: {}", path.display()),
149                ))
150            }
151            Some(Entry::Symlink { .. }) => {
152                return Err(io::Error::new(
153                    io::ErrorKind::NotADirectory,
154                    format!("not a directory: {}", path.display()),
155                ))
156            }
157            None if normalized.as_os_str().is_empty() => {
158                // Root directory
159            }
160            None => {
161                return Err(io::Error::new(
162                    io::ErrorKind::NotFound,
163                    format!("not found: {}", path.display()),
164                ))
165            }
166        }
167
168        // Find all direct children
169        let prefix = if normalized.as_os_str().is_empty() {
170            PathBuf::new()
171        } else {
172            normalized.clone()
173        };
174
175        let mut result = Vec::new();
176        for (entry_path, entry) in entries.iter() {
177            if let Some(parent) = entry_path.parent()
178                && parent == prefix && entry_path != &normalized
179                    && let Some(name) = entry_path.file_name() {
180                        let (entry_type, size, symlink_target) = match entry {
181                            Entry::File { data, .. } => (EntryType::File, data.len() as u64, None),
182                            Entry::Directory { .. } => (EntryType::Directory, 0, None),
183                            Entry::Symlink { target, .. } => (EntryType::Symlink, 0, Some(target.clone())),
184                        };
185                        result.push(DirEntry {
186                            name: name.to_string_lossy().into_owned(),
187                            entry_type,
188                            size,
189                            symlink_target,
190                        });
191                    }
192        }
193
194        // Sort for consistent ordering
195        result.sort_by(|a, b| a.name.cmp(&b.name));
196        Ok(result)
197    }
198
199    async fn stat(&self, path: &Path) -> io::Result<Metadata> {
200        let normalized = Self::normalize(path);
201
202        // Handle root directory
203        if normalized.as_os_str().is_empty() {
204            return Ok(Metadata {
205                is_dir: true,
206                is_file: false,
207                is_symlink: false,
208                size: 0,
209                modified: Some(SystemTime::now()),
210            });
211        }
212
213        // First, check what kind of entry this is
214        let entry_info: Option<(Metadata, Option<PathBuf>)> = {
215            let entries = self.entries.read().await;
216            match entries.get(&normalized) {
217                Some(Entry::File { data, modified }) => Some((
218                    Metadata {
219                        is_dir: false,
220                        is_file: true,
221                        is_symlink: false,
222                        size: data.len() as u64,
223                        modified: Some(*modified),
224                    },
225                    None, // No symlink target to follow
226                )),
227                Some(Entry::Directory { modified }) => Some((
228                    Metadata {
229                        is_dir: true,
230                        is_file: false,
231                        is_symlink: false,
232                        size: 0,
233                        modified: Some(*modified),
234                    },
235                    None,
236                )),
237                Some(Entry::Symlink { target, .. }) => {
238                    // Need to follow symlink - save the target
239                    Some((
240                        Metadata {
241                            is_dir: false,
242                            is_file: false,
243                            is_symlink: false,
244                            size: 0,
245                            modified: None, // Will be overwritten by target
246                        },
247                        Some(target.clone()),
248                    ))
249                }
250                None => None,
251            }
252        };
253
254        match entry_info {
255            Some((meta, None)) => Ok(meta),
256            Some((_, Some(target))) => {
257                // Follow the symlink
258                self.stat(&target).await
259            }
260            None => Err(io::Error::new(
261                io::ErrorKind::NotFound,
262                format!("not found: {}", path.display()),
263            )),
264        }
265    }
266
267    async fn lstat(&self, path: &Path) -> io::Result<Metadata> {
268        let normalized = Self::normalize(path);
269        let entries = self.entries.read().await;
270
271        // Handle root directory
272        if normalized.as_os_str().is_empty() {
273            return Ok(Metadata {
274                is_dir: true,
275                is_file: false,
276                is_symlink: false,
277                size: 0,
278                modified: Some(SystemTime::now()),
279            });
280        }
281
282        match entries.get(&normalized) {
283            Some(Entry::File { data, modified }) => Ok(Metadata {
284                is_dir: false,
285                is_file: true,
286                is_symlink: false,
287                size: data.len() as u64,
288                modified: Some(*modified),
289            }),
290            Some(Entry::Directory { modified }) => Ok(Metadata {
291                is_dir: true,
292                is_file: false,
293                is_symlink: false,
294                size: 0,
295                modified: Some(*modified),
296            }),
297            Some(Entry::Symlink { modified, .. }) => Ok(Metadata {
298                is_dir: false,
299                is_file: false,
300                is_symlink: true,
301                size: 0,
302                modified: Some(*modified),
303            }),
304            None => Err(io::Error::new(
305                io::ErrorKind::NotFound,
306                format!("not found: {}", path.display()),
307            )),
308        }
309    }
310
311    async fn read_link(&self, path: &Path) -> io::Result<PathBuf> {
312        let normalized = Self::normalize(path);
313        let entries = self.entries.read().await;
314
315        match entries.get(&normalized) {
316            Some(Entry::Symlink { target, .. }) => Ok(target.clone()),
317            Some(_) => Err(io::Error::new(
318                io::ErrorKind::InvalidInput,
319                format!("not a symbolic link: {}", path.display()),
320            )),
321            None => Err(io::Error::new(
322                io::ErrorKind::NotFound,
323                format!("not found: {}", path.display()),
324            )),
325        }
326    }
327
328    async fn symlink(&self, target: &Path, link: &Path) -> io::Result<()> {
329        let normalized = Self::normalize(link);
330
331        // Ensure parent directories exist
332        self.ensure_parents(&normalized).await?;
333
334        let mut entries = self.entries.write().await;
335
336        // Check if something already exists at this path
337        if entries.contains_key(&normalized) {
338            return Err(io::Error::new(
339                io::ErrorKind::AlreadyExists,
340                format!("file exists: {}", link.display()),
341            ));
342        }
343
344        entries.insert(
345            normalized,
346            Entry::Symlink {
347                target: target.to_path_buf(),
348                modified: SystemTime::now(),
349            },
350        );
351        Ok(())
352    }
353
354    async fn mkdir(&self, path: &Path) -> io::Result<()> {
355        let normalized = Self::normalize(path);
356
357        // Ensure parent directories exist
358        self.ensure_parents(&normalized).await?;
359
360        let mut entries = self.entries.write().await;
361
362        // Check if something already exists
363        if let Some(existing) = entries.get(&normalized) {
364            return match existing {
365                Entry::Directory { .. } => Ok(()), // Already exists, fine
366                Entry::File { .. } | Entry::Symlink { .. } => Err(io::Error::new(
367                    io::ErrorKind::AlreadyExists,
368                    format!("file exists: {}", path.display()),
369                )),
370            };
371        }
372
373        entries.insert(
374            normalized,
375            Entry::Directory {
376                modified: SystemTime::now(),
377            },
378        );
379        Ok(())
380    }
381
382    async fn remove(&self, path: &Path) -> io::Result<()> {
383        let normalized = Self::normalize(path);
384
385        if normalized.as_os_str().is_empty() {
386            return Err(io::Error::new(
387                io::ErrorKind::PermissionDenied,
388                "cannot remove root directory",
389            ));
390        }
391
392        let mut entries = self.entries.write().await;
393
394        // Check if it's a non-empty directory
395        if let Some(Entry::Directory { .. }) = entries.get(&normalized) {
396            // Check for children
397            let has_children = entries.keys().any(|k| {
398                k.parent() == Some(&normalized) && k != &normalized
399            });
400            if has_children {
401                return Err(io::Error::new(
402                    io::ErrorKind::DirectoryNotEmpty,
403                    format!("directory not empty: {}", path.display()),
404                ));
405            }
406        }
407
408        entries.remove(&normalized).ok_or_else(|| {
409            io::Error::new(
410                io::ErrorKind::NotFound,
411                format!("not found: {}", path.display()),
412            )
413        })?;
414        Ok(())
415    }
416
417    async fn rename(&self, from: &Path, to: &Path) -> io::Result<()> {
418        let from_normalized = Self::normalize(from);
419        let to_normalized = Self::normalize(to);
420
421        if from_normalized.as_os_str().is_empty() {
422            return Err(io::Error::new(
423                io::ErrorKind::PermissionDenied,
424                "cannot rename root directory",
425            ));
426        }
427
428        // Ensure parent directories exist for destination
429        drop(self.ensure_parents(&to_normalized).await);
430
431        let mut entries = self.entries.write().await;
432
433        // Get the source entry
434        let entry = entries.remove(&from_normalized).ok_or_else(|| {
435            io::Error::new(
436                io::ErrorKind::NotFound,
437                format!("not found: {}", from.display()),
438            )
439        })?;
440
441        // Check we're not overwriting a directory with a file or vice versa
442        if let Some(existing) = entries.get(&to_normalized) {
443            match (&entry, existing) {
444                (Entry::File { .. }, Entry::Directory { .. }) => {
445                    // Put the source back and error
446                    entries.insert(from_normalized, entry);
447                    return Err(io::Error::new(
448                        io::ErrorKind::IsADirectory,
449                        format!("destination is a directory: {}", to.display()),
450                    ));
451                }
452                (Entry::Directory { .. }, Entry::File { .. }) => {
453                    entries.insert(from_normalized, entry);
454                    return Err(io::Error::new(
455                        io::ErrorKind::NotADirectory,
456                        format!("destination is not a directory: {}", to.display()),
457                    ));
458                }
459                _ => {}
460            }
461        }
462
463        // For directories, we need to rename all children too
464        if matches!(entry, Entry::Directory { .. }) {
465            // Collect paths to rename (can't modify while iterating)
466            let children_to_rename: Vec<(PathBuf, Entry)> = entries
467                .iter()
468                .filter(|(k, _)| k.starts_with(&from_normalized) && *k != &from_normalized)
469                .map(|(k, v)| (k.clone(), v.clone()))
470                .collect();
471
472            // Remove old children and insert with new paths
473            for (old_path, child_entry) in children_to_rename {
474                entries.remove(&old_path);
475                let Ok(relative) = old_path.strip_prefix(&from_normalized) else {
476                    continue;
477                };
478                let new_path = to_normalized.join(relative);
479                entries.insert(new_path, child_entry);
480            }
481        }
482
483        // Insert at new location
484        entries.insert(to_normalized, entry);
485        Ok(())
486    }
487
488    fn read_only(&self) -> bool {
489        false
490    }
491}
492
493#[cfg(test)]
494mod tests {
495    use super::*;
496
497    #[tokio::test]
498    async fn test_write_and_read() {
499        let fs = MemoryFs::new();
500        fs.write(Path::new("test.txt"), b"hello world").await.unwrap();
501        let data = fs.read(Path::new("test.txt")).await.unwrap();
502        assert_eq!(data, b"hello world");
503    }
504
505    #[tokio::test]
506    async fn test_read_not_found() {
507        let fs = MemoryFs::new();
508        let result = fs.read(Path::new("nonexistent.txt")).await;
509        assert!(result.is_err());
510        assert_eq!(result.unwrap_err().kind(), io::ErrorKind::NotFound);
511    }
512
513    #[tokio::test]
514    async fn test_nested_directories() {
515        let fs = MemoryFs::new();
516        fs.write(Path::new("a/b/c/file.txt"), b"nested").await.unwrap();
517
518        // Should have created parent directories
519        let meta = fs.stat(Path::new("a")).await.unwrap();
520        assert!(meta.is_dir);
521
522        let meta = fs.stat(Path::new("a/b")).await.unwrap();
523        assert!(meta.is_dir);
524
525        let meta = fs.stat(Path::new("a/b/c")).await.unwrap();
526        assert!(meta.is_dir);
527
528        let data = fs.read(Path::new("a/b/c/file.txt")).await.unwrap();
529        assert_eq!(data, b"nested");
530    }
531
532    #[tokio::test]
533    async fn test_list_directory() {
534        let fs = MemoryFs::new();
535        fs.write(Path::new("a.txt"), b"a").await.unwrap();
536        fs.write(Path::new("b.txt"), b"b").await.unwrap();
537        fs.mkdir(Path::new("subdir")).await.unwrap();
538
539        let entries = fs.list(Path::new("")).await.unwrap();
540        assert_eq!(entries.len(), 3);
541
542        let names: Vec<_> = entries.iter().map(|e| &e.name).collect();
543        assert!(names.contains(&&"a.txt".to_string()));
544        assert!(names.contains(&&"b.txt".to_string()));
545        assert!(names.contains(&&"subdir".to_string()));
546    }
547
548    #[tokio::test]
549    async fn test_mkdir_and_stat() {
550        let fs = MemoryFs::new();
551        fs.mkdir(Path::new("mydir")).await.unwrap();
552
553        let meta = fs.stat(Path::new("mydir")).await.unwrap();
554        assert!(meta.is_dir);
555        assert!(!meta.is_file);
556    }
557
558    #[tokio::test]
559    async fn test_remove_file() {
560        let fs = MemoryFs::new();
561        fs.write(Path::new("file.txt"), b"data").await.unwrap();
562
563        fs.remove(Path::new("file.txt")).await.unwrap();
564
565        let result = fs.stat(Path::new("file.txt")).await;
566        assert!(result.is_err());
567    }
568
569    #[tokio::test]
570    async fn test_remove_empty_directory() {
571        let fs = MemoryFs::new();
572        fs.mkdir(Path::new("emptydir")).await.unwrap();
573
574        fs.remove(Path::new("emptydir")).await.unwrap();
575
576        let result = fs.stat(Path::new("emptydir")).await;
577        assert!(result.is_err());
578    }
579
580    #[tokio::test]
581    async fn test_remove_non_empty_directory_fails() {
582        let fs = MemoryFs::new();
583        fs.write(Path::new("dir/file.txt"), b"data").await.unwrap();
584
585        let result = fs.remove(Path::new("dir")).await;
586        assert!(result.is_err());
587        assert_eq!(result.unwrap_err().kind(), io::ErrorKind::DirectoryNotEmpty);
588    }
589
590    #[tokio::test]
591    async fn test_path_normalization() {
592        let fs = MemoryFs::new();
593        fs.write(Path::new("/a/b/c.txt"), b"data").await.unwrap();
594
595        // Various path forms should all work
596        let data1 = fs.read(Path::new("a/b/c.txt")).await.unwrap();
597        let data2 = fs.read(Path::new("/a/b/c.txt")).await.unwrap();
598        let data3 = fs.read(Path::new("a/./b/c.txt")).await.unwrap();
599        let data4 = fs.read(Path::new("a/b/../b/c.txt")).await.unwrap();
600
601        assert_eq!(data1, data2);
602        assert_eq!(data2, data3);
603        assert_eq!(data3, data4);
604    }
605
606    #[tokio::test]
607    async fn test_overwrite_file() {
608        let fs = MemoryFs::new();
609        fs.write(Path::new("file.txt"), b"first").await.unwrap();
610        fs.write(Path::new("file.txt"), b"second").await.unwrap();
611
612        let data = fs.read(Path::new("file.txt")).await.unwrap();
613        assert_eq!(data, b"second");
614    }
615
616    #[tokio::test]
617    async fn test_exists() {
618        let fs = MemoryFs::new();
619        assert!(!fs.exists(Path::new("nope.txt")).await);
620
621        fs.write(Path::new("yes.txt"), b"here").await.unwrap();
622        assert!(fs.exists(Path::new("yes.txt")).await);
623    }
624
625    #[tokio::test]
626    async fn test_rename_file() {
627        let fs = MemoryFs::new();
628        fs.write(Path::new("old.txt"), b"content").await.unwrap();
629
630        fs.rename(Path::new("old.txt"), Path::new("new.txt")).await.unwrap();
631
632        // New path exists with same content
633        let data = fs.read(Path::new("new.txt")).await.unwrap();
634        assert_eq!(data, b"content");
635
636        // Old path no longer exists
637        assert!(!fs.exists(Path::new("old.txt")).await);
638    }
639
640    #[tokio::test]
641    async fn test_rename_directory() {
642        let fs = MemoryFs::new();
643        fs.write(Path::new("dir/a.txt"), b"a").await.unwrap();
644        fs.write(Path::new("dir/b.txt"), b"b").await.unwrap();
645        fs.write(Path::new("dir/sub/c.txt"), b"c").await.unwrap();
646
647        fs.rename(Path::new("dir"), Path::new("renamed")).await.unwrap();
648
649        // New paths exist
650        assert!(fs.exists(Path::new("renamed")).await);
651        assert!(fs.exists(Path::new("renamed/a.txt")).await);
652        assert!(fs.exists(Path::new("renamed/b.txt")).await);
653        assert!(fs.exists(Path::new("renamed/sub/c.txt")).await);
654
655        // Old paths don't exist
656        assert!(!fs.exists(Path::new("dir")).await);
657        assert!(!fs.exists(Path::new("dir/a.txt")).await);
658
659        // Content preserved
660        let data = fs.read(Path::new("renamed/a.txt")).await.unwrap();
661        assert_eq!(data, b"a");
662    }
663
664    #[tokio::test]
665    async fn test_rename_not_found() {
666        let fs = MemoryFs::new();
667        let result = fs.rename(Path::new("nonexistent"), Path::new("dest")).await;
668        assert!(result.is_err());
669        assert_eq!(result.unwrap_err().kind(), io::ErrorKind::NotFound);
670    }
671
672    // --- Symlink tests ---
673
674    #[tokio::test]
675    async fn test_symlink_create_and_read_link() {
676        let fs = MemoryFs::new();
677        fs.write(Path::new("target.txt"), b"content").await.unwrap();
678        fs.symlink(Path::new("target.txt"), Path::new("link.txt")).await.unwrap();
679
680        // read_link returns the raw target
681        let target = fs.read_link(Path::new("link.txt")).await.unwrap();
682        assert_eq!(target, Path::new("target.txt"));
683    }
684
685    #[tokio::test]
686    async fn test_symlink_read_follows_link() {
687        let fs = MemoryFs::new();
688        fs.write(Path::new("target.txt"), b"hello from target").await.unwrap();
689        fs.symlink(Path::new("target.txt"), Path::new("link.txt")).await.unwrap();
690
691        // Reading through symlink should return target's content
692        let data = fs.read(Path::new("link.txt")).await.unwrap();
693        assert_eq!(data, b"hello from target");
694    }
695
696    #[tokio::test]
697    async fn test_symlink_stat_follows_link() {
698        let fs = MemoryFs::new();
699        fs.write(Path::new("target.txt"), b"12345").await.unwrap();
700        fs.symlink(Path::new("target.txt"), Path::new("link.txt")).await.unwrap();
701
702        // stat follows symlinks - should report file metadata
703        let meta = fs.stat(Path::new("link.txt")).await.unwrap();
704        assert!(meta.is_file);
705        assert!(!meta.is_symlink);
706        assert_eq!(meta.size, 5);
707    }
708
709    #[tokio::test]
710    async fn test_symlink_lstat_returns_symlink_info() {
711        let fs = MemoryFs::new();
712        fs.write(Path::new("target.txt"), b"content").await.unwrap();
713        fs.symlink(Path::new("target.txt"), Path::new("link.txt")).await.unwrap();
714
715        // lstat does not follow symlinks
716        let meta = fs.lstat(Path::new("link.txt")).await.unwrap();
717        assert!(meta.is_symlink);
718        assert!(!meta.is_file);
719        assert!(!meta.is_dir);
720    }
721
722    #[tokio::test]
723    async fn test_symlink_in_list() {
724        let fs = MemoryFs::new();
725        fs.write(Path::new("file.txt"), b"content").await.unwrap();
726        fs.symlink(Path::new("file.txt"), Path::new("link.txt")).await.unwrap();
727        fs.mkdir(Path::new("dir")).await.unwrap();
728
729        let entries = fs.list(Path::new("")).await.unwrap();
730        assert_eq!(entries.len(), 3);
731
732        // Find the symlink entry
733        let link_entry = entries.iter().find(|e| e.name == "link.txt").unwrap();
734        assert_eq!(link_entry.entry_type, EntryType::Symlink);
735        assert_eq!(link_entry.symlink_target, Some(PathBuf::from("file.txt")));
736    }
737
738    #[tokio::test]
739    async fn test_symlink_broken_link() {
740        let fs = MemoryFs::new();
741        // Create symlink to non-existent target
742        fs.symlink(Path::new("nonexistent.txt"), Path::new("broken.txt")).await.unwrap();
743
744        // read_link still works
745        let target = fs.read_link(Path::new("broken.txt")).await.unwrap();
746        assert_eq!(target, Path::new("nonexistent.txt"));
747
748        // lstat works (the symlink exists)
749        let meta = fs.lstat(Path::new("broken.txt")).await.unwrap();
750        assert!(meta.is_symlink);
751
752        // stat fails (target doesn't exist)
753        let result = fs.stat(Path::new("broken.txt")).await;
754        assert!(result.is_err());
755        assert_eq!(result.unwrap_err().kind(), io::ErrorKind::NotFound);
756
757        // read fails (target doesn't exist)
758        let result = fs.read(Path::new("broken.txt")).await;
759        assert!(result.is_err());
760    }
761
762    #[tokio::test]
763    async fn test_symlink_read_link_on_non_symlink_fails() {
764        let fs = MemoryFs::new();
765        fs.write(Path::new("file.txt"), b"content").await.unwrap();
766
767        let result = fs.read_link(Path::new("file.txt")).await;
768        assert!(result.is_err());
769        assert_eq!(result.unwrap_err().kind(), io::ErrorKind::InvalidInput);
770    }
771
772    #[tokio::test]
773    async fn test_symlink_already_exists() {
774        let fs = MemoryFs::new();
775        fs.write(Path::new("existing.txt"), b"content").await.unwrap();
776
777        // Can't create symlink over existing file
778        let result = fs.symlink(Path::new("target"), Path::new("existing.txt")).await;
779        assert!(result.is_err());
780        assert_eq!(result.unwrap_err().kind(), io::ErrorKind::AlreadyExists);
781    }
782
783    // --- Edge case tests ---
784
785    #[tokio::test]
786    async fn test_symlink_chain() {
787        // a -> b -> c -> file.txt
788        let fs = MemoryFs::new();
789        fs.write(Path::new("file.txt"), b"end of chain").await.unwrap();
790        fs.symlink(Path::new("file.txt"), Path::new("c")).await.unwrap();
791        fs.symlink(Path::new("c"), Path::new("b")).await.unwrap();
792        fs.symlink(Path::new("b"), Path::new("a")).await.unwrap();
793
794        // Reading through chain should work
795        let data = fs.read(Path::new("a")).await.unwrap();
796        assert_eq!(data, b"end of chain");
797
798        // stat through chain should report file
799        let meta = fs.stat(Path::new("a")).await.unwrap();
800        assert!(meta.is_file);
801    }
802
803    #[tokio::test]
804    async fn test_symlink_to_directory() {
805        let fs = MemoryFs::new();
806        fs.mkdir(Path::new("realdir")).await.unwrap();
807        fs.write(Path::new("realdir/file.txt"), b"inside dir").await.unwrap();
808        fs.symlink(Path::new("realdir"), Path::new("linkdir")).await.unwrap();
809
810        // stat follows symlink - should see directory
811        let meta = fs.stat(Path::new("linkdir")).await.unwrap();
812        assert!(meta.is_dir);
813
814        // Note: listing through symlink requires following in list(),
815        // which we don't currently support (symlink to dir returns NotADirectory)
816    }
817
818    #[tokio::test]
819    async fn test_symlink_relative_path_stored_as_is() {
820        let fs = MemoryFs::new();
821        fs.mkdir(Path::new("subdir")).await.unwrap();
822        fs.write(Path::new("subdir/target.txt"), b"content").await.unwrap();
823
824        // Store a relative path in the symlink
825        fs.symlink(Path::new("../subdir/target.txt"), Path::new("subdir/link.txt")).await.unwrap();
826
827        // read_link returns the path as stored
828        let target = fs.read_link(Path::new("subdir/link.txt")).await.unwrap();
829        assert_eq!(target.to_string_lossy(), "../subdir/target.txt");
830    }
831
832    #[tokio::test]
833    async fn test_symlink_absolute_path() {
834        let fs = MemoryFs::new();
835        fs.write(Path::new("target.txt"), b"content").await.unwrap();
836
837        // Store absolute path
838        fs.symlink(Path::new("/target.txt"), Path::new("link.txt")).await.unwrap();
839
840        let target = fs.read_link(Path::new("link.txt")).await.unwrap();
841        assert_eq!(target.to_string_lossy(), "/target.txt");
842
843        // Following should work (normalize strips leading /)
844        let data = fs.read(Path::new("link.txt")).await.unwrap();
845        assert_eq!(data, b"content");
846    }
847
848    #[tokio::test]
849    async fn test_symlink_remove() {
850        let fs = MemoryFs::new();
851        fs.write(Path::new("target.txt"), b"content").await.unwrap();
852        fs.symlink(Path::new("target.txt"), Path::new("link.txt")).await.unwrap();
853
854        // Remove symlink (not the target)
855        fs.remove(Path::new("link.txt")).await.unwrap();
856
857        // Symlink gone
858        assert!(!fs.exists(Path::new("link.txt")).await);
859
860        // Target still exists
861        assert!(fs.exists(Path::new("target.txt")).await);
862    }
863
864    #[tokio::test]
865    async fn test_symlink_overwrite_target_content() {
866        let fs = MemoryFs::new();
867        fs.write(Path::new("target.txt"), b"original").await.unwrap();
868        fs.symlink(Path::new("target.txt"), Path::new("link.txt")).await.unwrap();
869
870        // Modify target
871        fs.write(Path::new("target.txt"), b"modified").await.unwrap();
872
873        // Reading through link shows new content
874        let data = fs.read(Path::new("link.txt")).await.unwrap();
875        assert_eq!(data, b"modified");
876    }
877
878    #[tokio::test]
879    async fn test_symlink_empty_name() {
880        let fs = MemoryFs::new();
881        fs.write(Path::new("target.txt"), b"content").await.unwrap();
882
883        // Symlink with empty path components in target
884        fs.symlink(Path::new("./target.txt"), Path::new("link.txt")).await.unwrap();
885
886        let target = fs.read_link(Path::new("link.txt")).await.unwrap();
887        assert_eq!(target.to_string_lossy(), "./target.txt");
888    }
889
890    #[tokio::test]
891    async fn test_symlink_nested_creation() {
892        let fs = MemoryFs::new();
893        // Symlink in non-existent directory should create parents
894        fs.symlink(Path::new("target"), Path::new("a/b/c/link")).await.unwrap();
895
896        // Parents created
897        let meta = fs.stat(Path::new("a/b")).await.unwrap();
898        assert!(meta.is_dir);
899
900        // Symlink exists (lstat)
901        let meta = fs.lstat(Path::new("a/b/c/link")).await.unwrap();
902        assert!(meta.is_symlink);
903    }
904
905    #[tokio::test]
906    async fn test_symlink_read_link_not_found() {
907        let fs = MemoryFs::new();
908
909        let result = fs.read_link(Path::new("nonexistent")).await;
910        assert!(result.is_err());
911        assert_eq!(result.unwrap_err().kind(), io::ErrorKind::NotFound);
912    }
913
914    #[tokio::test]
915    async fn test_symlink_read_link_on_directory() {
916        let fs = MemoryFs::new();
917        fs.mkdir(Path::new("dir")).await.unwrap();
918
919        let result = fs.read_link(Path::new("dir")).await;
920        assert!(result.is_err());
921        assert_eq!(result.unwrap_err().kind(), io::ErrorKind::InvalidInput);
922    }
923}