Skip to main content

strat9_kernel/vfs/
ramfs_scheme.rs

1//! In-kernel RAM filesystem — mounts on `/` to provide a writable root.
2//!
3//! ## Design
4//!
5//! A single `SpinLock<RamState>` protects all state.  The lock is held only
6//! for the duration of each scheme call (no blocking I/O inside), so lock
7//! contention is negligible.
8//!
9//! Inodes are stored in a flat `BTreeMap<u64, RamInode>` keyed by inode number.
10//! Directories hold a `BTreeMap<String, u64>` of `(name → ino)` children.
11//! Files hold their content in a heap-allocated `Vec<u8>`.
12//!
13//! Inode 1 is always the root directory (`/`).
14//!
15//! ## Path convention
16//!
17//! `path` arguments arrive **relative to the mount point** (i.e. the leading
18//! `/` has already been stripped by the mount table resolver).  An empty
19//! string therefore means the root directory.
20
21use alloc::{collections::BTreeMap, string::String, vec::Vec};
22use core::sync::atomic::{AtomicU64, Ordering};
23
24use crate::{
25    process::current_task_clone,
26    sync::SpinLock,
27    syscall::error::SyscallError,
28    vfs::scheme::{
29        DirEntry, FileFlags, FileStat, OpenFlags, OpenResult, Scheme, DEV_RAMFS, DT_DIR, DT_LNK,
30        DT_REG,
31    },
32};
33
34// ─── Inode numbering ─────────────────────────────────────────────────────────
35
36const INO_ROOT: u64 = 1;
37const INO_FIRST: u64 = 2;
38
39// ─── Inode types ─────────────────────────────────────────────────────────────
40
41enum RamKind {
42    File { data: Vec<u8> },
43    Dir { children: BTreeMap<String, u64> },
44    Symlink { target: String },
45}
46
47struct RamInode {
48    #[allow(dead_code)]
49    ino: u64,
50    kind: RamKind,
51    /// Unix permission bits including file-type high bits (e.g. `0o100_644`).
52    mode: u32,
53    uid: u32,
54    gid: u32,
55    atime_ns: u64,
56    mtime_ns: u64,
57    ctime_ns: u64,
58    rdev: u64,
59}
60
61impl RamInode {
62    /// Returns whether dir.
63    fn is_dir(&self) -> bool {
64        matches!(self.kind, RamKind::Dir { .. })
65    }
66
67    /// Performs the byte size operation.
68    fn byte_size(&self) -> u64 {
69        match &self.kind {
70            RamKind::File { data } => data.len() as u64,
71            RamKind::Dir { .. } => 0,
72            RamKind::Symlink { target } => target.len() as u64,
73        }
74    }
75}
76
77// ─── Filesystem state ────────────────────────────────────────────────────────
78
79struct RamState {
80    inodes: BTreeMap<u64, RamInode>,
81}
82
83impl RamState {
84    /// Performs the now ns operation.
85    fn now_ns() -> u64 {
86        crate::syscall::time::current_time_ns()
87    }
88
89    /// Performs the current ids operation.
90    fn current_ids() -> (u32, u32) {
91        if let Some(task) = current_task_clone() {
92            (
93                task.uid.load(Ordering::Relaxed),
94                task.gid.load(Ordering::Relaxed),
95            )
96        } else {
97            (0, 0)
98        }
99    }
100
101    /// Creates a new instance.
102    fn new() -> Self {
103        let mut inodes = BTreeMap::new();
104        let now = Self::now_ns();
105        inodes.insert(
106            INO_ROOT,
107            RamInode {
108                ino: INO_ROOT,
109                kind: RamKind::Dir {
110                    children: BTreeMap::new(),
111                },
112                mode: 0o040_755, // drwxr-xr-x
113                uid: 0,
114                gid: 0,
115                atime_ns: now,
116                mtime_ns: now,
117                ctime_ns: now,
118                rdev: 0,
119            },
120        );
121        RamState { inodes }
122    }
123
124    /// Resolve a relative path to its inode number.
125    ///
126    /// An empty (or all-slash) `path` resolves to the root directory.
127    fn lookup(&self, path: &str) -> Option<u64> {
128        let path = path.trim_matches('/');
129        if path.is_empty() {
130            return Some(INO_ROOT);
131        }
132        let mut cur = INO_ROOT;
133        for part in path.split('/') {
134            if part.is_empty() {
135                continue;
136            }
137            let inode = self.inodes.get(&cur)?;
138            match &inode.kind {
139                RamKind::Dir { children } => {
140                    cur = *children.get(part)?;
141                }
142                _ => return None,
143            }
144        }
145        Some(cur)
146    }
147
148    /// Return `(parent_ino, final_component)` for a path.
149    ///
150    /// Returns `None` if the parent directory does not exist.
151    fn lookup_parent<'a>(&self, path: &'a str) -> Option<(u64, &'a str)> {
152        let path = path.trim_matches('/');
153        let (parent_path, name) = match path.rfind('/') {
154            Some(pos) => (&path[..pos], &path[pos + 1..]),
155            None => ("", path),
156        };
157        let parent_ino = self.lookup(parent_path)?;
158        Some((parent_ino, name))
159    }
160
161    /// Returns whether any name ref is available.
162    fn has_any_name_ref(&self, ino: u64) -> bool {
163        self.inodes.values().any(|inode| {
164            if let RamKind::Dir { children } = &inode.kind {
165                children.values().any(|&child| child == ino)
166            } else {
167                false
168            }
169        })
170    }
171
172    /// Performs the link count operation.
173    fn link_count(&self, ino: u64) -> u32 {
174        let mut count = 0u32;
175        for inode in self.inodes.values() {
176            if let RamKind::Dir { children } = &inode.kind {
177                for &child in children.values() {
178                    if child == ino {
179                        count = count.saturating_add(1);
180                    }
181                }
182            }
183        }
184        count
185    }
186
187    /// Performs the dir contains ino operation.
188    fn dir_contains_ino(&self, ancestor_ino: u64, candidate_ino: u64) -> bool {
189        if ancestor_ino == candidate_ino {
190            return true;
191        }
192        let mut stack = Vec::new();
193        stack.push(ancestor_ino);
194        while let Some(cur) = stack.pop() {
195            let Some(inode) = self.inodes.get(&cur) else {
196                continue;
197            };
198            let RamKind::Dir { children } = &inode.kind else {
199                continue;
200            };
201            for &child in children.values() {
202                if child == candidate_ino {
203                    return true;
204                }
205                if self
206                    .inodes
207                    .get(&child)
208                    .map(|n| matches!(n.kind, RamKind::Dir { .. }))
209                    .unwrap_or(false)
210                {
211                    stack.push(child);
212                }
213            }
214        }
215        false
216    }
217}
218
219// ─── RamfsScheme ─────────────────────────────────────────────────────────────
220
221/// Kernel-resident RAM filesystem implementing the `Scheme` trait.
222pub struct RamfsScheme {
223    state: SpinLock<RamState>,
224    next_ino: AtomicU64,
225}
226
227impl RamfsScheme {
228    /// Creates a new instance.
229    pub fn new() -> Self {
230        RamfsScheme {
231            state: SpinLock::new(RamState::new()),
232            next_ino: AtomicU64::new(INO_FIRST),
233        }
234    }
235
236    /// Allocates ino.
237    fn alloc_ino(&self) -> u64 {
238        self.next_ino.fetch_add(1, Ordering::Relaxed)
239    }
240
241    /// Ensure that `path` (relative to mount root) exists as a directory.
242    ///
243    /// Silently succeeds if the directory already exists.
244    pub fn ensure_dir(&self, path: &str) {
245        let _ = self.create_directory(path.trim_matches('/'), 0o755);
246    }
247
248    /// Insert a read-only file into the root.
249    ///
250    /// Convenience helper used during `vfs::init()` to pre-populate files.
251    pub fn insert_file(&self, name: &str, content: &[u8]) {
252        let ino = self.alloc_ino();
253        let mut st = self.state.lock();
254        let now = RamState::now_ns();
255        st.inodes.insert(
256            ino,
257            RamInode {
258                ino,
259                kind: RamKind::File {
260                    data: content.to_vec(),
261                },
262                mode: 0o100_444, // -r--r--r--
263                uid: 0,
264                gid: 0,
265                atime_ns: now,
266                mtime_ns: now,
267                ctime_ns: now,
268                rdev: 0,
269            },
270        );
271        if let Some(root) = st.inodes.get_mut(&INO_ROOT) {
272            if let RamKind::Dir { ref mut children } = root.kind {
273                children.insert(String::from(name), ino);
274                root.mtime_ns = now;
275                root.ctime_ns = now;
276            }
277        }
278    }
279}
280
281impl Scheme for RamfsScheme {
282    // ── open ─────────────────────────────────────────────────────────────────
283
284    /// Performs the open operation.
285    fn open(&self, path: &str, flags: OpenFlags) -> Result<OpenResult, SyscallError> {
286        let path = path.trim_matches('/');
287
288        if flags.contains(OpenFlags::CREATE) {
289            let mut st = self.state.lock();
290            let now = RamState::now_ns();
291
292            if let Some(ino) = st.lookup(path) {
293                // Entry already exists — succeed (POSIX O_CREAT without O_EXCL).
294                let inode = st.inodes.get_mut(&ino).unwrap();
295                if inode.is_dir() {
296                    inode.atime_ns = now;
297                    return Ok(OpenResult {
298                        file_id: ino,
299                        size: Some(0),
300                        flags: FileFlags::DIRECTORY,
301                    });
302                }
303                if flags.contains(OpenFlags::TRUNCATE) {
304                    if let RamKind::File { ref mut data } = inode.kind {
305                        data.clear();
306                    }
307                    inode.mtime_ns = now;
308                    inode.ctime_ns = now;
309                }
310                inode.atime_ns = now;
311                let size = inode.byte_size();
312                return Ok(OpenResult {
313                    file_id: ino,
314                    size: Some(size),
315                    flags: FileFlags::empty(),
316                });
317            }
318
319            // Create the new file.
320            let (parent_ino, name) = st.lookup_parent(path).ok_or(SyscallError::NotFound)?;
321            if name.is_empty() {
322                return Err(SyscallError::InvalidArgument);
323            }
324            let new_ino = self.next_ino.fetch_add(1, Ordering::Relaxed);
325            let (uid, gid) = RamState::current_ids();
326            st.inodes.insert(
327                new_ino,
328                RamInode {
329                    ino: new_ino,
330                    kind: RamKind::File { data: Vec::new() },
331                    mode: 0o100_644,
332                    uid,
333                    gid,
334                    atime_ns: now,
335                    mtime_ns: now,
336                    ctime_ns: now,
337                    rdev: 0,
338                },
339            );
340            if let Some(parent) = st.inodes.get_mut(&parent_ino) {
341                if let RamKind::Dir { ref mut children } = parent.kind {
342                    children.insert(String::from(name), new_ino);
343                    parent.mtime_ns = now;
344                    parent.ctime_ns = now;
345                }
346            }
347            return Ok(OpenResult {
348                file_id: new_ino,
349                size: Some(0),
350                flags: FileFlags::empty(),
351            });
352        }
353
354        // Normal open (no O_CREAT).
355        let mut st = self.state.lock();
356        let now = RamState::now_ns();
357        let ino = st.lookup(path).ok_or(SyscallError::NotFound)?;
358        let inode = st.inodes.get_mut(&ino).unwrap();
359        inode.atime_ns = now;
360
361        if inode.is_dir() {
362            Ok(OpenResult {
363                file_id: ino,
364                size: Some(0),
365                flags: FileFlags::DIRECTORY,
366            })
367        } else {
368            Ok(OpenResult {
369                file_id: ino,
370                size: Some(inode.byte_size()),
371                flags: FileFlags::empty(),
372            })
373        }
374    }
375
376    // ── read ─────────────────────────────────────────────────────────────────
377
378    /// Performs the read operation.
379    fn read(&self, file_id: u64, offset: u64, buf: &mut [u8]) -> Result<usize, SyscallError> {
380        let mut st = self.state.lock();
381        let now = RamState::now_ns();
382        let inode = st.inodes.get_mut(&file_id).ok_or(SyscallError::BadHandle)?;
383        match &inode.kind {
384            RamKind::File { data } => {
385                let start = offset as usize;
386                if start >= data.len() {
387                    inode.atime_ns = now;
388                    return Ok(0);
389                }
390                let n = (data.len() - start).min(buf.len());
391                buf[..n].copy_from_slice(&data[start..start + n]);
392                inode.atime_ns = now;
393                Ok(n)
394            }
395            RamKind::Dir { .. } | RamKind::Symlink { .. } => Err(SyscallError::InvalidArgument),
396        }
397    }
398
399    // ── write ────────────────────────────────────────────────────────────────
400
401    /// Performs the write operation.
402    fn write(&self, file_id: u64, offset: u64, buf: &[u8]) -> Result<usize, SyscallError> {
403        let mut st = self.state.lock();
404        let now = RamState::now_ns();
405        let inode = st.inodes.get_mut(&file_id).ok_or(SyscallError::BadHandle)?;
406        match &mut inode.kind {
407            RamKind::File { ref mut data } => {
408                let start = offset as usize;
409                let end = start + buf.len();
410                if end > data.len() {
411                    data.resize(end, 0);
412                }
413                data[start..end].copy_from_slice(buf);
414                inode.mtime_ns = now;
415                inode.ctime_ns = now;
416                Ok(buf.len())
417            }
418            RamKind::Dir { .. } | RamKind::Symlink { .. } => Err(SyscallError::InvalidArgument),
419        }
420    }
421
422    // ── close ────────────────────────────────────────────────────────────────
423
424    /// Performs the close operation.
425    fn close(&self, _file_id: u64) -> Result<(), SyscallError> {
426        Ok(()) // stateless — nothing to clean up
427    }
428
429    // ── size ─────────────────────────────────────────────────────────────────
430
431    /// Performs the size operation.
432    fn size(&self, file_id: u64) -> Result<u64, SyscallError> {
433        let st = self.state.lock();
434        let inode = st.inodes.get(&file_id).ok_or(SyscallError::BadHandle)?;
435        Ok(inode.byte_size())
436    }
437
438    // ── truncate ─────────────────────────────────────────────────────────────
439
440    /// Performs the truncate operation.
441    fn truncate(&self, file_id: u64, new_size: u64) -> Result<(), SyscallError> {
442        let mut st = self.state.lock();
443        let now = RamState::now_ns();
444        let inode = st.inodes.get_mut(&file_id).ok_or(SyscallError::BadHandle)?;
445        match &mut inode.kind {
446            RamKind::File { ref mut data } => {
447                data.resize(new_size as usize, 0);
448                inode.mtime_ns = now;
449                inode.ctime_ns = now;
450                Ok(())
451            }
452            RamKind::Dir { .. } | RamKind::Symlink { .. } => Err(SyscallError::InvalidArgument),
453        }
454    }
455
456    // ── create_file ──────────────────────────────────────────────────────────
457
458    /// Creates file.
459    fn create_file(&self, path: &str, mode: u32) -> Result<OpenResult, SyscallError> {
460        let path = path.trim_matches('/');
461        let mut st = self.state.lock();
462        let now = RamState::now_ns();
463        let (uid, gid) = RamState::current_ids();
464
465        if st.lookup(path).is_some() {
466            return Err(SyscallError::AlreadyExists);
467        }
468
469        let (parent_ino, name) = st.lookup_parent(path).ok_or(SyscallError::NotFound)?;
470        if name.is_empty() {
471            return Err(SyscallError::InvalidArgument);
472        }
473
474        let new_ino = self.next_ino.fetch_add(1, Ordering::Relaxed);
475        // Ensure the mode has the regular-file type bits set.
476        let file_mode = (mode & 0o7777) | 0o100_000;
477        st.inodes.insert(
478            new_ino,
479            RamInode {
480                ino: new_ino,
481                kind: RamKind::File { data: Vec::new() },
482                mode: file_mode,
483                uid,
484                gid,
485                atime_ns: now,
486                mtime_ns: now,
487                ctime_ns: now,
488                rdev: 0,
489            },
490        );
491        if let Some(parent) = st.inodes.get_mut(&parent_ino) {
492            if let RamKind::Dir { ref mut children } = parent.kind {
493                children.insert(String::from(name), new_ino);
494                parent.mtime_ns = now;
495                parent.ctime_ns = now;
496            }
497        }
498
499        Ok(OpenResult {
500            file_id: new_ino,
501            size: Some(0),
502            flags: FileFlags::empty(),
503        })
504    }
505
506    // ── create_directory ─────────────────────────────────────────────────────
507
508    /// Creates directory.
509    fn create_directory(&self, path: &str, mode: u32) -> Result<OpenResult, SyscallError> {
510        let path = path.trim_matches('/');
511        let mut st = self.state.lock();
512        let now = RamState::now_ns();
513        let (uid, gid) = RamState::current_ids();
514
515        // Idempotent: return Ok if it already exists as a directory.
516        if let Some(ino) = st.lookup(path) {
517            let inode = st.inodes.get(&ino).unwrap();
518            if inode.is_dir() {
519                return Ok(OpenResult {
520                    file_id: ino,
521                    size: Some(0),
522                    flags: FileFlags::DIRECTORY,
523                });
524            }
525            return Err(SyscallError::AlreadyExists);
526        }
527
528        let (parent_ino, name) = st.lookup_parent(path).ok_or(SyscallError::NotFound)?;
529        if name.is_empty() {
530            return Err(SyscallError::InvalidArgument);
531        }
532
533        let new_ino = self.next_ino.fetch_add(1, Ordering::Relaxed);
534        let dir_mode = (mode & 0o7777) | 0o040_000;
535        st.inodes.insert(
536            new_ino,
537            RamInode {
538                ino: new_ino,
539                kind: RamKind::Dir {
540                    children: BTreeMap::new(),
541                },
542                mode: dir_mode,
543                uid,
544                gid,
545                atime_ns: now,
546                mtime_ns: now,
547                ctime_ns: now,
548                rdev: 0,
549            },
550        );
551        if let Some(parent) = st.inodes.get_mut(&parent_ino) {
552            if let RamKind::Dir { ref mut children } = parent.kind {
553                children.insert(String::from(name), new_ino);
554                parent.mtime_ns = now;
555                parent.ctime_ns = now;
556            }
557        }
558
559        Ok(OpenResult {
560            file_id: new_ino,
561            size: Some(0),
562            flags: FileFlags::DIRECTORY,
563        })
564    }
565
566    // ── unlink ───────────────────────────────────────────────────────────────
567
568    /// Performs the unlink operation.
569    fn unlink(&self, path: &str) -> Result<(), SyscallError> {
570        let path = path.trim_matches('/');
571        if path.is_empty() {
572            return Err(SyscallError::PermissionDenied); // cannot remove root
573        }
574
575        let mut st = self.state.lock();
576        let now = RamState::now_ns();
577        let ino = st.lookup(path).ok_or(SyscallError::NotFound)?;
578
579        // Refuse to remove a non-empty directory.
580        if let Some(inode) = st.inodes.get(&ino) {
581            if let RamKind::Dir { children } = &inode.kind {
582                if !children.is_empty() {
583                    return Err(SyscallError::NotEmpty);
584                }
585            }
586        }
587
588        let (parent_ino, name) = st.lookup_parent(path).ok_or(SyscallError::NotFound)?;
589        if let Some(parent) = st.inodes.get_mut(&parent_ino) {
590            if let RamKind::Dir { ref mut children } = parent.kind {
591                children.remove(name);
592                parent.mtime_ns = now;
593                parent.ctime_ns = now;
594            }
595        }
596        if st
597            .inodes
598            .get(&ino)
599            .map(|inode| inode.is_dir())
600            .unwrap_or(false)
601            || !st.has_any_name_ref(ino)
602        {
603            st.inodes.remove(&ino);
604        }
605
606        Ok(())
607    }
608
609    // ── stat ─────────────────────────────────────────────────────────────────
610
611    /// Performs the stat operation.
612    fn stat(&self, file_id: u64) -> Result<FileStat, SyscallError> {
613        let st = self.state.lock();
614        let inode = st.inodes.get(&file_id).ok_or(SyscallError::BadHandle)?;
615
616        let (st_mode, st_size, st_nlink) = match &inode.kind {
617            RamKind::Dir { children } => (inode.mode, 0u64, 2 + children.len() as u32),
618            RamKind::File { data } => (inode.mode, data.len() as u64, st.link_count(file_id)),
619            RamKind::Symlink { target } => {
620                (inode.mode, target.len() as u64, st.link_count(file_id))
621            }
622        };
623
624        Ok(FileStat {
625            st_dev: DEV_RAMFS,
626            st_ino: file_id,
627            st_mode,
628            st_nlink,
629            st_uid: inode.uid,
630            st_gid: inode.gid,
631            st_rdev: inode.rdev,
632            st_size,
633            st_blksize: 4096,
634            st_blocks: (st_size + 511) / 512,
635            st_atime: strat9_abi::data::TimeSpec::from_nanos(inode.atime_ns),
636            st_mtime: strat9_abi::data::TimeSpec::from_nanos(inode.mtime_ns),
637            st_ctime: strat9_abi::data::TimeSpec::from_nanos(inode.ctime_ns),
638            ..FileStat::zeroed()
639        })
640    }
641
642    // ── readdir ──────────────────────────────────────────────────────────────
643
644    /// Performs the readdir operation.
645    fn readdir(&self, file_id: u64) -> Result<Vec<DirEntry>, SyscallError> {
646        let mut st = self.state.lock();
647        let now = RamState::now_ns();
648        let children_snapshot: Vec<(String, u64)> = {
649            let inode = st.inodes.get(&file_id).ok_or(SyscallError::BadHandle)?;
650            match &inode.kind {
651                RamKind::Dir { children } => children
652                    .iter()
653                    .map(|(name, &child_ino)| (name.clone(), child_ino))
654                    .collect(),
655                RamKind::File { .. } | RamKind::Symlink { .. } => {
656                    return Err(SyscallError::InvalidArgument)
657                }
658            }
659        };
660
661        let mut entries = Vec::with_capacity(children_snapshot.len());
662        for (name, child_ino) in children_snapshot {
663            let file_type = match st.inodes.get(&child_ino).map(|c| &c.kind) {
664                Some(RamKind::Dir { .. }) => DT_DIR,
665                Some(RamKind::Symlink { .. }) => DT_LNK,
666                _ => DT_REG,
667            };
668            entries.push(DirEntry {
669                ino: child_ino,
670                file_type,
671                name,
672            });
673        }
674
675        if let Some(inode) = st.inodes.get_mut(&file_id) {
676            inode.atime_ns = now;
677        }
678        Ok(entries)
679    }
680
681    // ── rename ──────────────────────────────────────────────────────────────
682
683    /// Performs the rename operation.
684    fn rename(&self, old_path: &str, new_path: &str) -> Result<(), SyscallError> {
685        let old_path = old_path.trim_matches('/');
686        let new_path = new_path.trim_matches('/');
687        if old_path.is_empty() || new_path.is_empty() {
688            return Err(SyscallError::InvalidArgument);
689        }
690
691        let mut st = self.state.lock();
692        let now = RamState::now_ns();
693        let ino = st.lookup(old_path).ok_or(SyscallError::NotFound)?;
694
695        let (old_parent, old_name) = st.lookup_parent(old_path).ok_or(SyscallError::NotFound)?;
696        let old_name = String::from(old_name);
697
698        let (new_parent, new_name) = st.lookup_parent(new_path).ok_or(SyscallError::NotFound)?;
699        let new_name = String::from(new_name);
700        if st
701            .inodes
702            .get(&ino)
703            .map(|n| n.is_dir() && st.dir_contains_ino(ino, new_parent))
704            .unwrap_or(false)
705        {
706            return Err(SyscallError::InvalidArgument);
707        }
708        let existing = st.lookup(new_path);
709
710        if let Some(existing) = existing {
711            if existing == ino {
712                return Ok(());
713            }
714            if let Some(inode) = st.inodes.get(&existing) {
715                if let RamKind::Dir { children } = &inode.kind {
716                    if !children.is_empty() {
717                        return Err(SyscallError::NotSupported);
718                    }
719                }
720            }
721        }
722
723        if let Some(parent) = st.inodes.get_mut(&old_parent) {
724            if let RamKind::Dir { ref mut children } = parent.kind {
725                children.remove(&old_name);
726                parent.mtime_ns = now;
727                parent.ctime_ns = now;
728            }
729        }
730        if let Some(parent) = st.inodes.get_mut(&new_parent) {
731            if let RamKind::Dir { ref mut children } = parent.kind {
732                children.insert(new_name, ino);
733                parent.mtime_ns = now;
734                parent.ctime_ns = now;
735            }
736        }
737        if let Some(inode) = st.inodes.get_mut(&ino) {
738            inode.ctime_ns = now;
739        }
740        if let Some(existing) = existing {
741            if !st.has_any_name_ref(existing) {
742                st.inodes.remove(&existing);
743            }
744        }
745        Ok(())
746    }
747
748    // ── chmod ───────────────────────────────────────────────────────────────
749
750    /// Performs the chmod operation.
751    fn chmod(&self, path: &str, mode: u32) -> Result<(), SyscallError> {
752        let path = path.trim_matches('/');
753        let mut st = self.state.lock();
754        let now = RamState::now_ns();
755        let ino = st.lookup(path).ok_or(SyscallError::NotFound)?;
756        let inode = st.inodes.get_mut(&ino).ok_or(SyscallError::NotFound)?;
757        let type_bits = inode.mode & 0o170_000;
758        inode.mode = type_bits | (mode & 0o7777);
759        inode.ctime_ns = now;
760        Ok(())
761    }
762
763    // ── fchmod ──────────────────────────────────────────────────────────────
764
765    /// Performs the fchmod operation.
766    fn fchmod(&self, file_id: u64, mode: u32) -> Result<(), SyscallError> {
767        let mut st = self.state.lock();
768        let now = RamState::now_ns();
769        let inode = st.inodes.get_mut(&file_id).ok_or(SyscallError::BadHandle)?;
770        let type_bits = inode.mode & 0o170_000;
771        inode.mode = type_bits | (mode & 0o7777);
772        inode.ctime_ns = now;
773        Ok(())
774    }
775
776    // ── link ────────────────────────────────────────────────────────────────
777
778    /// Performs the link operation.
779    fn link(&self, old_path: &str, new_path: &str) -> Result<(), SyscallError> {
780        let old_path = old_path.trim_matches('/');
781        let new_path = new_path.trim_matches('/');
782
783        let mut st = self.state.lock();
784        let now = RamState::now_ns();
785        let ino = st.lookup(old_path).ok_or(SyscallError::NotFound)?;
786
787        if st.inodes.get(&ino).map(|i| i.is_dir()).unwrap_or(false) {
788            return Err(SyscallError::PermissionDenied);
789        }
790        if st.lookup(new_path).is_some() {
791            return Err(SyscallError::AlreadyExists);
792        }
793
794        let (parent_ino, name) = st.lookup_parent(new_path).ok_or(SyscallError::NotFound)?;
795        if let Some(parent) = st.inodes.get_mut(&parent_ino) {
796            if let RamKind::Dir { ref mut children } = parent.kind {
797                children.insert(String::from(name), ino);
798                parent.mtime_ns = now;
799                parent.ctime_ns = now;
800            }
801        }
802        if let Some(inode) = st.inodes.get_mut(&ino) {
803            inode.ctime_ns = now;
804        }
805        Ok(())
806    }
807
808    // ── symlink ─────────────────────────────────────────────────────────────
809
810    /// Performs the symlink operation.
811    fn symlink(&self, target: &str, link_path: &str) -> Result<(), SyscallError> {
812        let link_path = link_path.trim_matches('/');
813        if link_path.is_empty() {
814            return Err(SyscallError::InvalidArgument);
815        }
816
817        let mut st = self.state.lock();
818        let now = RamState::now_ns();
819        let (uid, gid) = RamState::current_ids();
820        if st.lookup(link_path).is_some() {
821            return Err(SyscallError::AlreadyExists);
822        }
823
824        let (parent_ino, name) = st.lookup_parent(link_path).ok_or(SyscallError::NotFound)?;
825        let new_ino = self.next_ino.fetch_add(1, Ordering::Relaxed);
826        st.inodes.insert(
827            new_ino,
828            RamInode {
829                ino: new_ino,
830                kind: RamKind::Symlink {
831                    target: String::from(target),
832                },
833                mode: 0o120_777,
834                uid,
835                gid,
836                atime_ns: now,
837                mtime_ns: now,
838                ctime_ns: now,
839                rdev: 0,
840            },
841        );
842        if let Some(parent) = st.inodes.get_mut(&parent_ino) {
843            if let RamKind::Dir { ref mut children } = parent.kind {
844                children.insert(String::from(name), new_ino);
845                parent.mtime_ns = now;
846                parent.ctime_ns = now;
847            }
848        }
849        Ok(())
850    }
851
852    // ── readlink ────────────────────────────────────────────────────────────
853
854    /// Performs the readlink operation.
855    fn readlink(&self, path: &str) -> Result<String, SyscallError> {
856        let path = path.trim_matches('/');
857        let mut st = self.state.lock();
858        let now = RamState::now_ns();
859        let ino = st.lookup(path).ok_or(SyscallError::NotFound)?;
860        let inode = st.inodes.get_mut(&ino).ok_or(SyscallError::NotFound)?;
861        match &inode.kind {
862            RamKind::Symlink { target } => {
863                inode.atime_ns = now;
864                Ok(target.clone())
865            }
866            _ => Err(SyscallError::InvalidArgument),
867        }
868    }
869}