Skip to main content

kaish_kernel/vfs/
memory.rs

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