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