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