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