Skip to main content

fs_core/
fs_singlethread.rs

1//! Single-threaded filesystem implementation using Rc<RefCell>
2//!
3//! This module provides a non-thread-safe filesystem that uses Rc<RefCell>
4//! instead of Arc<RwLock>. It's suitable for:
5//! - vfs-adapter (runs inside WASM boundary)
6//! - vfs-rpc-server (runs inside WASM boundary)
7//!
8//! For thread-safe usage (e.g., vfs-host), use the thread-safe module instead.
9
10#[cfg(feature = "std")]
11use std::cell::RefCell;
12#[cfg(feature = "std")]
13use std::collections::HashMap;
14#[cfg(feature = "std")]
15use std::rc::Rc;
16
17#[cfg(not(feature = "std"))]
18use alloc::collections::BTreeMap;
19#[cfg(not(feature = "std"))]
20use alloc::rc::Rc;
21#[cfg(not(feature = "std"))]
22use core::cell::RefCell;
23
24use crate::error::FsError;
25use crate::handle::FileHandle;
26use crate::inode::{FileContent, Inode, Metadata};
27use crate::time::{MonotonicCounter, TimeProvider};
28use crate::types::*;
29
30// Import logging macros
31#[cfg(feature = "logging")]
32use crate::{debug, error, trace};
33
34/// Type alias for the internal map type
35#[cfg(feature = "std")]
36type Map<K, V> = HashMap<K, V>;
37#[cfg(not(feature = "std"))]
38type Map<K, V> = BTreeMap<K, V>;
39
40/// Type alias for inode reference (single-threaded version)
41pub type InodeRef = Rc<RefCell<Inode>>;
42
43/// Main filesystem structure (single-threaded, uses Rc<RefCell>)
44pub struct Fs<T: TimeProvider = MonotonicCounter> {
45    pub(crate) next_inode: InodeId,
46    pub(crate) fd_table: Map<Fd, FileHandle>,
47    pub(crate) inode_table: Map<InodeId, Rc<RefCell<Inode>>>,
48    pub(crate) root_inode: InodeId,
49    pub(crate) time_provider: T,
50}
51
52impl<T: TimeProvider> Fs<T> {
53    pub fn with_time_provider(time_provider: T) -> Self {
54        let mut fs = Self {
55            next_inode: 1,
56            fd_table: Map::new(),
57            inode_table: Map::new(),
58            root_inode: 0,
59            time_provider,
60        };
61
62        // Create root directory inode with proper timestamps
63        let timestamp = fs.time_provider.now();
64        let mut root_inode = Inode::new_dir(0);
65        root_inode.metadata.created = timestamp;
66        root_inode.metadata.modified = timestamp;
67
68        let root = Rc::new(RefCell::new(root_inode));
69        fs.inode_table.insert(0, root);
70        fs.root_inode = 0;
71
72        fs
73    }
74
75    fn allocate_inode(&mut self) -> InodeId {
76        let id = self.next_inode;
77        self.next_inode += 1;
78        id
79    }
80
81    fn allocate_fd(&mut self) -> Fd {
82        // Find the lowest available file descriptor starting from 3
83        // This implements POSIX-compliant FD reuse
84        let mut fd = 3;
85        while self.fd_table.contains_key(&fd) {
86            fd += 1;
87        }
88        fd
89    }
90
91    fn find_inode(
92        &self,
93        parent_inode: &Rc<RefCell<Inode>>,
94        name: &str,
95    ) -> Result<Rc<RefCell<Inode>>, FsError> {
96        let parent = parent_inode.borrow();
97
98        match &parent.content {
99            FileContent::Dir(entries) => {
100                if let Some(&inode_id) = entries.get(name) {
101                    if let Some(inode) = self.inode_table.get(&inode_id) {
102                        Ok(inode.clone())
103                    } else {
104                        Err(FsError::NotFound)
105                    }
106                } else {
107                    Err(FsError::NotFound)
108                }
109            }
110            FileContent::File(_) => Err(FsError::NotADirectory),
111        }
112    }
113
114    fn create_inode(
115        &mut self,
116        parent_inode: &Rc<RefCell<Inode>>,
117        name: &str,
118        is_dir: bool,
119    ) -> Result<Rc<RefCell<Inode>>, FsError> {
120        let mut parent = parent_inode.borrow_mut();
121
122        match &mut parent.content {
123            FileContent::Dir(entries) => {
124                // Check if already exists
125                if entries.contains_key(name) {
126                    return Err(FsError::AlreadyExists);
127                }
128
129                // Create new inode
130                let new_inode_id = self.allocate_inode();
131                let timestamp = self.time_provider.now();
132                let mut new_inode = if is_dir {
133                    Inode::new_dir(new_inode_id)
134                } else {
135                    Inode::new_file(new_inode_id)
136                };
137
138                new_inode.metadata.created = timestamp;
139                new_inode.metadata.modified = timestamp;
140
141                let new_inode_rc = Rc::new(RefCell::new(new_inode));
142                entries.insert(name.into(), new_inode_id);
143                self.inode_table.insert(new_inode_id, new_inode_rc.clone());
144
145                // Update parent directory's modification time
146                parent.metadata.modified = timestamp;
147
148                Ok(new_inode_rc)
149            }
150            FileContent::File(_) => Err(FsError::NotADirectory),
151        }
152    }
153
154    pub fn open_path(&mut self, path: &str) -> Result<Fd, FsError> {
155        self.open_path_with_flags(path, O_RDWR | O_CREAT)
156    }
157
158    pub fn open_at(&mut self, dir_fd: Fd, path: &str, flags: u32) -> Result<Fd, FsError> {
159        debug!(
160            "open_at: dir_fd={}, path={}, flags={:#x}",
161            dir_fd, path, flags
162        );
163
164        // Reject absolute paths. open_at only accepts relative paths
165        if path.starts_with('/') {
166            error!("open_at: absolute path not allowed");
167            return Err(FsError::InvalidArgument);
168        }
169
170        // Get the directory file descriptor
171        #[allow(clippy::unnecessary_lazy_evaluations)]
172        let dir_handle = self.fd_table.get(&dir_fd).ok_or_else(|| {
173            error!("open_at: bad directory file descriptor {}", dir_fd);
174            FsError::BadFileDescriptor
175        })?;
176
177        // Get the directory inode
178        let dir_inode = self
179            .inode_table
180            .get(&dir_handle.inode_id)
181            .ok_or(FsError::NotFound)?
182            .clone();
183
184        // Verify it's a directory
185        {
186            let inode = dir_inode.borrow();
187            if matches!(inode.content, FileContent::File(_)) {
188                error!("open_at: dir_fd {} is not a directory", dir_fd);
189                return Err(FsError::NotADirectory);
190            }
191        }
192
193        // Handle empty path: refers to the directory itself
194        if path.is_empty() {
195            error!("open_at: empty path not supported yet");
196            return Err(FsError::InvalidArgument);
197        }
198
199        let comps: alloc::vec::Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
200
201        if comps.is_empty() {
202            return Err(FsError::InvalidArgument);
203        }
204
205        // Start from the directory fd, not root
206        let mut current_inode = dir_inode;
207
208        // Navigate to parent directory (all components except last)
209        for comp in comps.iter().take(comps.len() - 1) {
210            current_inode = self.find_inode(&current_inode, comp)?;
211
212            // Verify it's a directory
213            let inode = current_inode.borrow();
214            if matches!(inode.content, FileContent::File(_)) {
215                return Err(FsError::NotADirectory);
216            }
217        }
218
219        // Handle final component (file name)
220        let filename = comps[comps.len() - 1];
221        let file_inode = match self.find_inode(&current_inode, filename) {
222            Ok(inode) => inode,
223            Err(FsError::NotFound) if flags & O_CREAT != 0 => {
224                // Create new file if O_CREAT is set
225                self.create_inode(&current_inode, filename, false)?
226            }
227            Err(e) => return Err(e),
228        };
229
230        // Check if target is a directory
231        {
232            let inode = file_inode.borrow();
233            if matches!(inode.content, FileContent::Dir(_)) {
234                let access_mode = flags & 0x3;
235                // POSIX: opening directory with write access is not allowed
236                if access_mode == O_WRONLY || access_mode == O_RDWR {
237                    return Err(FsError::IsADirectory);
238                }
239                // O_TRUNC on directory is also not allowed
240                if flags & O_TRUNC != 0 {
241                    return Err(FsError::IsADirectory);
242                }
243            }
244        }
245
246        // Handle O_TRUNC: truncate file to 0 bytes if flag is set
247        if flags & O_TRUNC != 0 {
248            let access_mode = flags & 0x3;
249            if access_mode == O_RDONLY {
250                return Err(FsError::InvalidArgument);
251            }
252            let mut inode = file_inode.borrow_mut();
253            if let FileContent::File(storage) = &mut inode.content {
254                storage.truncate(0);
255                inode.metadata.size = 0;
256                inode.metadata.modified = self.time_provider.now();
257            }
258        }
259
260        let inode_id = file_inode.borrow().id;
261        let handle = FileHandle::new(inode_id, 0, flags);
262
263        let fd = self.allocate_fd();
264        self.fd_table.insert(fd, handle);
265        debug!("open_at: allocated fd={} for inode={}", fd, inode_id);
266        Ok(fd)
267    }
268
269    pub fn open_path_with_flags(&mut self, path: &str, flags: u32) -> Result<Fd, FsError> {
270        debug!("open_path_with_flags: path={}, flags={:#x}", path, flags);
271
272        if path.is_empty() {
273            error!("open_path_with_flags: empty path");
274            return Err(FsError::InvalidArgument);
275        }
276
277        let comps: alloc::vec::Vec<&str> = path
278            .trim_start_matches('/')
279            .split('/')
280            .filter(|s| !s.is_empty())
281            .collect();
282
283        // Special case: opening root directory "/"
284        if comps.is_empty() {
285            debug!("open_path_with_flags: opening root directory");
286            let handle = FileHandle::new(self.root_inode, 0, flags);
287            let fd = self.allocate_fd();
288            self.fd_table.insert(fd, handle);
289            debug!("open_path_with_flags: allocated fd={} for root", fd);
290            return Ok(fd);
291        }
292
293        let root_inode = self
294            .inode_table
295            .get(&self.root_inode)
296            .ok_or(FsError::NotFound)?
297            .clone();
298        let mut current_inode = root_inode;
299
300        // Navigate to parent directory (all components except last)
301        for comp in comps.iter().take(comps.len() - 1) {
302            current_inode = self.find_inode(&current_inode, comp)?;
303
304            // Verify it's a directory
305            let inode = current_inode.borrow();
306            if matches!(inode.content, FileContent::File(_)) {
307                return Err(FsError::NotADirectory);
308            }
309        }
310
311        // Handle final component (file name)
312        let filename = comps[comps.len() - 1];
313        let file_inode = match self.find_inode(&current_inode, filename) {
314            Ok(inode) => inode,
315            Err(FsError::NotFound) if flags & O_CREAT != 0 => {
316                // Create new file if O_CREAT is set
317                self.create_inode(&current_inode, filename, false)?
318            }
319            Err(e) => return Err(e),
320        };
321
322        // Check if target is a directory
323        {
324            let inode = file_inode.borrow();
325            if matches!(inode.content, FileContent::Dir(_)) {
326                let access_mode = flags & 0x3;
327                // POSIX: opening directory with write access is not allowed
328                if access_mode == O_WRONLY || access_mode == O_RDWR {
329                    return Err(FsError::IsADirectory);
330                }
331                // O_TRUNC on directory is also not allowed
332                if flags & O_TRUNC != 0 {
333                    return Err(FsError::IsADirectory);
334                }
335            }
336        }
337
338        // Handle O_TRUNC: truncate file to 0 bytes if flag is set
339        // POSIX requires write permission for O_TRUNC
340        if flags & O_TRUNC != 0 {
341            let access_mode = flags & 0x3;
342            if access_mode == O_RDONLY {
343                return Err(FsError::InvalidArgument);
344            }
345            let mut inode = file_inode.borrow_mut();
346            if let FileContent::File(storage) = &mut inode.content {
347                storage.truncate(0);
348                inode.metadata.size = 0;
349                inode.metadata.modified = self.time_provider.now();
350            }
351        }
352
353        let inode_id = file_inode.borrow().id;
354        let handle = FileHandle::new(inode_id, 0, flags);
355
356        let fd = self.allocate_fd();
357        self.fd_table.insert(fd, handle);
358        debug!(
359            "open_path_with_flags: allocated fd={} for inode={}",
360            fd, inode_id
361        );
362        Ok(fd)
363    }
364
365    pub fn write(&mut self, fd: Fd, buf: &[u8]) -> Result<usize, FsError> {
366        trace!("write: fd={}, len={}", fd, buf.len());
367
368        #[allow(clippy::unnecessary_lazy_evaluations)]
369        let handle = self.fd_table.get_mut(&fd).ok_or_else(|| {
370            error!("write: bad file descriptor {}", fd);
371            FsError::BadFileDescriptor
372        })?;
373
374        // Check write permission
375        let access_mode = handle.flags & 0x3;
376        if access_mode == O_RDONLY {
377            return Err(FsError::PermissionDenied);
378        }
379
380        let inode = self
381            .inode_table
382            .get(&handle.inode_id)
383            .ok_or(FsError::NotFound)?;
384        let mut inode_ref = inode.borrow_mut();
385
386        match &mut inode_ref.content {
387            FileContent::File(storage) => {
388                // Handle O_APPEND: move position to end of file before writing
389                let pos = if handle.flags & O_APPEND != 0 {
390                    let file_size = storage.size();
391                    handle.set_position(file_size as u64);
392                    file_size
393                } else {
394                    handle.get_position() as usize
395                };
396
397                let n = storage.write(pos, buf);
398                handle.set_position(handle.get_position() + n as u64);
399                inode_ref.metadata.size = storage.size() as u64;
400                inode_ref.metadata.modified = self.time_provider.now();
401                debug!("write: fd={}, wrote {} bytes at pos {}", fd, n, pos);
402                Ok(n)
403            }
404            FileContent::Dir(_) => {
405                error!("write: fd={} is a directory", fd);
406                Err(FsError::BadFileDescriptor)
407            }
408        }
409    }
410
411    /// Append data to file atomically (always writes at end of file)
412    pub fn append_write(&mut self, fd: Fd, buf: &[u8]) -> Result<usize, FsError> {
413        trace!("append_write: fd={}, len={}", fd, buf.len());
414
415        #[allow(clippy::unnecessary_lazy_evaluations)]
416        let handle = self.fd_table.get_mut(&fd).ok_or_else(|| {
417            error!("append_write: bad file descriptor {}", fd);
418            FsError::BadFileDescriptor
419        })?;
420
421        // Check write permission
422        let access_mode = handle.flags & 0x3;
423        if access_mode == O_RDONLY {
424            return Err(FsError::PermissionDenied);
425        }
426
427        let inode = self
428            .inode_table
429            .get(&handle.inode_id)
430            .ok_or(FsError::NotFound)?;
431        let mut inode_ref = inode.borrow_mut();
432
433        match &mut inode_ref.content {
434            FileContent::File(storage) => {
435                // Always write at the end of file (atomic append)
436                let pos = storage.size();
437                let n = storage.write(pos, buf);
438                handle.set_position((pos + n) as u64);
439                inode_ref.metadata.size = storage.size() as u64;
440                inode_ref.metadata.modified = self.time_provider.now();
441                debug!(
442                    "append_write: fd={}, appended {} bytes at pos {}",
443                    fd, n, pos
444                );
445                Ok(n)
446            }
447            FileContent::Dir(_) => {
448                error!("append_write: fd={} is a directory", fd);
449                Err(FsError::BadFileDescriptor)
450            }
451        }
452    }
453
454    pub fn read(&mut self, fd: Fd, out: &mut [u8]) -> Result<usize, FsError> {
455        trace!("read: fd={}, buf_len={}", fd, out.len());
456
457        #[allow(clippy::unnecessary_lazy_evaluations)]
458        let handle = self.fd_table.get_mut(&fd).ok_or_else(|| {
459            error!("read: bad file descriptor {}", fd);
460            FsError::BadFileDescriptor
461        })?;
462
463        // Check read permission
464        let access_mode = handle.flags & 0x3;
465        if access_mode == O_WRONLY {
466            return Err(FsError::PermissionDenied);
467        }
468
469        let inode = self
470            .inode_table
471            .get(&handle.inode_id)
472            .ok_or(FsError::NotFound)?;
473        let inode_ref = inode.borrow();
474
475        match &inode_ref.content {
476            FileContent::File(storage) => {
477                let pos = handle.get_position() as usize;
478                let n = storage.read(pos, out);
479                handle.set_position(handle.get_position() + n as u64);
480                debug!("read: fd={}, read {} bytes at pos {}", fd, n, pos);
481                Ok(n)
482            }
483            FileContent::Dir(_) => {
484                error!("read: fd={} is a directory", fd);
485                Err(FsError::BadFileDescriptor)
486            }
487        }
488    }
489
490    pub fn ftruncate(&mut self, fd: Fd, size: u64) -> Result<(), FsError> {
491        let handle = self.fd_table.get(&fd).ok_or(FsError::BadFileDescriptor)?;
492
493        // Check write permission (truncate requires write access)
494        let access_mode = handle.flags & 0x3;
495        if access_mode == O_RDONLY {
496            return Err(FsError::PermissionDenied);
497        }
498
499        let inode = self
500            .inode_table
501            .get(&handle.inode_id)
502            .ok_or(FsError::NotFound)?;
503        let mut inode_ref = inode.borrow_mut();
504
505        match &mut inode_ref.content {
506            FileContent::File(storage) => {
507                storage.truncate(size as usize);
508                inode_ref.metadata.size = size;
509                inode_ref.metadata.modified = self.time_provider.now();
510                Ok(())
511            }
512            FileContent::Dir(_) => Err(FsError::BadFileDescriptor),
513        }
514    }
515
516    pub fn close(&mut self, fd: Fd) -> Result<(), FsError> {
517        trace!("close: fd={}", fd);
518
519        #[allow(clippy::unnecessary_lazy_evaluations)]
520        self.fd_table.remove(&fd).ok_or_else(|| {
521            error!("close: bad file descriptor {}", fd);
522            FsError::BadFileDescriptor
523        })?;
524
525        debug!("close: fd={} closed successfully", fd);
526        Ok(())
527    }
528
529    pub fn stat(&self, path: &str) -> Result<Metadata, FsError> {
530        if path.is_empty() {
531            return Err(FsError::InvalidArgument);
532        }
533
534        let comps: alloc::vec::Vec<&str> = path
535            .trim_start_matches('/')
536            .split('/')
537            .filter(|s| !s.is_empty())
538            .collect();
539
540        if comps.is_empty() {
541            // Root directory
542            let root = self
543                .inode_table
544                .get(&self.root_inode)
545                .ok_or(FsError::NotFound)?;
546            return Ok(root.borrow().metadata);
547        }
548
549        let root_inode = self
550            .inode_table
551            .get(&self.root_inode)
552            .ok_or(FsError::NotFound)?
553            .clone();
554        let mut current_inode = root_inode;
555
556        // Navigate through all components
557        for comp in comps.iter() {
558            current_inode = self.find_inode(&current_inode, comp)?;
559        }
560
561        let metadata = current_inode.borrow().metadata;
562        Ok(metadata)
563    }
564
565    pub fn fstat(&self, fd: Fd) -> Result<Metadata, FsError> {
566        let handle = self.fd_table.get(&fd).ok_or(FsError::BadFileDescriptor)?;
567        let inode = self
568            .inode_table
569            .get(&handle.inode_id)
570            .ok_or(FsError::NotFound)?;
571        Ok(inode.borrow().metadata)
572    }
573
574    pub fn seek(&mut self, fd: Fd, offset: i64, whence: i32) -> Result<u64, FsError> {
575        trace!("seek: fd={}, offset={}, whence={}", fd, offset, whence);
576
577        const SEEK_SET: i32 = 0;
578        const SEEK_CUR: i32 = 1;
579        const SEEK_END: i32 = 2;
580
581        #[allow(clippy::unnecessary_lazy_evaluations)]
582        let handle = self.fd_table.get_mut(&fd).ok_or_else(|| {
583            error!("seek: bad file descriptor {}", fd);
584            FsError::BadFileDescriptor
585        })?;
586        let inode = self
587            .inode_table
588            .get(&handle.inode_id)
589            .ok_or(FsError::NotFound)?;
590        let inode_ref = inode.borrow();
591
592        match &inode_ref.content {
593            FileContent::File(storage) => {
594                let new_pos = match whence {
595                    SEEK_SET => {
596                        if offset < 0 {
597                            return Err(FsError::InvalidArgument);
598                        }
599                        offset as u64
600                    }
601                    SEEK_CUR => {
602                        let new_pos_signed = handle.get_position() as i64 + offset;
603                        if new_pos_signed < 0 {
604                            return Err(FsError::InvalidArgument);
605                        }
606                        new_pos_signed as u64
607                    }
608                    SEEK_END => {
609                        let new_pos_signed = storage.size() as i64 + offset;
610                        if new_pos_signed < 0 {
611                            return Err(FsError::InvalidArgument);
612                        }
613                        new_pos_signed as u64
614                    }
615                    _ => {
616                        error!("seek: invalid whence {}", whence);
617                        return Err(FsError::InvalidArgument);
618                    }
619                };
620
621                handle.set_position(new_pos);
622                debug!("seek: fd={}, new_pos={}", fd, new_pos);
623                Ok(new_pos)
624            }
625            FileContent::Dir(_) => {
626                error!("seek: fd={} is a directory", fd);
627                Err(FsError::BadFileDescriptor)
628            }
629        }
630    }
631
632    pub fn mkdir(&mut self, path: &str) -> Result<(), FsError> {
633        debug!("mkdir: path={}", path);
634
635        if path.is_empty() {
636            error!("mkdir: empty path");
637            return Err(FsError::InvalidArgument);
638        }
639
640        let comps: alloc::vec::Vec<&str> = path
641            .trim_start_matches('/')
642            .split('/')
643            .filter(|s| !s.is_empty())
644            .collect();
645
646        if comps.is_empty() {
647            return Err(FsError::InvalidArgument);
648        }
649
650        let root_inode = self
651            .inode_table
652            .get(&self.root_inode)
653            .ok_or(FsError::NotFound)?
654            .clone();
655        let mut current_inode = root_inode;
656
657        // Navigate to parent directory (all components except last)
658        for comp in comps.iter().take(comps.len() - 1) {
659            current_inode = self.find_inode(&current_inode, comp)?;
660
661            // Verify it's a directory
662            let inode = current_inode.borrow();
663            if matches!(inode.content, FileContent::File(_)) {
664                return Err(FsError::NotADirectory);
665            }
666        }
667
668        // Create the final directory component
669        let dirname = comps[comps.len() - 1];
670        self.create_inode(&current_inode, dirname, true)?;
671        debug!("mkdir: created directory {}", path);
672        Ok(())
673    }
674
675    pub fn mkdir_p(&mut self, path: &str) -> Result<(), FsError> {
676        if path.is_empty() {
677            return Err(FsError::InvalidArgument);
678        }
679
680        let comps: alloc::vec::Vec<&str> = path
681            .trim_start_matches('/')
682            .split('/')
683            .filter(|s| !s.is_empty())
684            .collect();
685
686        if comps.is_empty() {
687            return Ok(()); // Root already exists
688        }
689
690        let root_inode = self
691            .inode_table
692            .get(&self.root_inode)
693            .ok_or(FsError::NotFound)?
694            .clone();
695        let mut current_inode = root_inode;
696
697        // Create all components as needed
698        for comp in comps.iter() {
699            current_inode = match self.find_inode(&current_inode, comp) {
700                Ok(inode) => {
701                    // Verify it's a directory
702                    {
703                        let borrowed = inode.borrow();
704                        if matches!(borrowed.content, FileContent::File(_)) {
705                            return Err(FsError::NotADirectory);
706                        }
707                    }
708                    inode
709                }
710                Err(FsError::NotFound) => {
711                    // Create directory
712                    self.create_inode(&current_inode, comp, true)?
713                }
714                Err(e) => return Err(e),
715            };
716        }
717
718        Ok(())
719    }
720
721    pub fn unlink(&mut self, path: &str) -> Result<(), FsError> {
722        debug!("unlink: path={}", path);
723
724        if path.is_empty() {
725            error!("unlink: empty path");
726            return Err(FsError::InvalidArgument);
727        }
728
729        let comps: alloc::vec::Vec<&str> = path
730            .trim_start_matches('/')
731            .split('/')
732            .filter(|s| !s.is_empty())
733            .collect();
734
735        if comps.is_empty() {
736            // Cannot unlink root directory
737            return Err(FsError::InvalidArgument);
738        }
739
740        let root_inode = self
741            .inode_table
742            .get(&self.root_inode)
743            .ok_or(FsError::NotFound)?
744            .clone();
745        let mut current_inode = root_inode;
746
747        // Navigate to parent directory (all components except last)
748        for comp in comps.iter().take(comps.len() - 1) {
749            current_inode = self.find_inode(&current_inode, comp)?;
750
751            // Verify it's a directory
752            let inode = current_inode.borrow();
753            if matches!(inode.content, FileContent::File(_)) {
754                return Err(FsError::NotADirectory);
755            }
756        }
757
758        // Get the filename to delete
759        let filename = comps[comps.len() - 1];
760
761        // Check if the file exists and get its inode
762        let target_inode = self.find_inode(&current_inode, filename)?;
763
764        // Check if it's a directory: use IsADirectory error
765        {
766            let target = target_inode.borrow();
767            if matches!(target.content, FileContent::Dir(_)) {
768                return Err(FsError::IsADirectory);
769            }
770        }
771
772        // Remove from parent directory
773        let mut parent = current_inode.borrow_mut();
774        match &mut parent.content {
775            FileContent::Dir(entries) => {
776                entries.remove(filename);
777
778                // Update parent directory's modification time
779                let timestamp = self.time_provider.now();
780                parent.metadata.modified = timestamp;
781
782                debug!("unlink: removed {}", path);
783                Ok(())
784            }
785            FileContent::File(_) => Err(FsError::NotADirectory),
786        }
787        // Note: The inode will be automatically cleaned up when the Rc ref count reaches 0
788    }
789
790    pub fn readdir(&self, path: &str) -> Result<alloc::vec::Vec<alloc::string::String>, FsError> {
791        if path.is_empty() {
792            return Err(FsError::InvalidArgument);
793        }
794
795        let comps: alloc::vec::Vec<&str> = path
796            .trim_start_matches('/')
797            .split('/')
798            .filter(|s| !s.is_empty())
799            .collect();
800
801        let root_inode = self
802            .inode_table
803            .get(&self.root_inode)
804            .ok_or(FsError::NotFound)?
805            .clone();
806        let mut current_inode = root_inode;
807
808        // Navigate through all components
809        for comp in comps.iter() {
810            current_inode = self.find_inode(&current_inode, comp)?;
811        }
812
813        // Check if it's a directory
814        let inode = current_inode.borrow();
815        match &inode.content {
816            FileContent::Dir(entries) => {
817                let mut result = alloc::vec::Vec::new();
818                for name in entries.keys() {
819                    result.push(name.clone());
820                }
821                Ok(result)
822            }
823            FileContent::File(_) => Err(FsError::NotADirectory),
824        }
825    }
826
827    /// Read directory entries from an open file descriptor
828    /// Returns a list of (name, is_dir) tuples
829    pub fn readdir_fd(
830        &self,
831        fd: Fd,
832    ) -> Result<alloc::vec::Vec<(alloc::string::String, bool)>, FsError> {
833        // Get the file handle
834        let handle = self.fd_table.get(&fd).ok_or(FsError::BadFileDescriptor)?;
835
836        // Get the inode
837        let inode = self
838            .inode_table
839            .get(&handle.inode_id)
840            .ok_or(FsError::NotFound)?;
841
842        // Check if it's a directory
843        let inode_ref = inode.borrow();
844        match &inode_ref.content {
845            FileContent::Dir(entries) => {
846                let mut result = alloc::vec::Vec::new();
847                for (name, child_inode_id) in entries.iter() {
848                    // Get the child inode to check if it's a directory
849                    if let Some(child_inode) = self.inode_table.get(child_inode_id) {
850                        let is_dir = child_inode.borrow().metadata.is_dir;
851                        result.push((name.clone(), is_dir));
852                    }
853                }
854                Ok(result)
855            }
856            FileContent::File(_) => Err(FsError::NotADirectory),
857        }
858    }
859
860    /// Remove an empty directory
861    pub fn rmdir(&mut self, path: &str) -> Result<(), FsError> {
862        debug!("rmdir: path={}", path);
863
864        if path.is_empty() {
865            error!("rmdir: empty path");
866            return Err(FsError::InvalidArgument);
867        }
868
869        let comps: alloc::vec::Vec<&str> = path
870            .trim_start_matches('/')
871            .split('/')
872            .filter(|s| !s.is_empty())
873            .collect();
874
875        if comps.is_empty() {
876            // Cannot remove root directory
877            return Err(FsError::InvalidArgument);
878        }
879
880        let root_inode = self
881            .inode_table
882            .get(&self.root_inode)
883            .ok_or(FsError::NotFound)?
884            .clone();
885        let mut current_inode = root_inode;
886
887        // Navigate to parent directory (all components except last)
888        for comp in comps.iter().take(comps.len() - 1) {
889            current_inode = self.find_inode(&current_inode, comp)?;
890
891            // Verify it's a directory
892            let inode = current_inode.borrow();
893            if matches!(inode.content, FileContent::File(_)) {
894                return Err(FsError::NotADirectory);
895            }
896        }
897
898        // Get the directory name to delete
899        let dirname = comps[comps.len() - 1];
900
901        // Check if the directory exists and get its inode
902        let target_inode = self.find_inode(&current_inode, dirname)?;
903
904        // Check if it's a directory
905        {
906            let target = target_inode.borrow();
907            match &target.content {
908                FileContent::File(_) => {
909                    return Err(FsError::NotADirectory);
910                }
911                FileContent::Dir(entries) => {
912                    // Check if directory is empty
913                    if !entries.is_empty() {
914                        error!("rmdir: directory not empty");
915                        return Err(FsError::NotEmpty);
916                    }
917                }
918            }
919        }
920
921        // Remove from parent directory
922        let mut parent = current_inode.borrow_mut();
923        match &mut parent.content {
924            FileContent::Dir(entries) => {
925                entries.remove(dirname);
926
927                // Update parent directory's modification time
928                let timestamp = self.time_provider.now();
929                parent.metadata.modified = timestamp;
930
931                debug!("rmdir: removed {}", path);
932                Ok(())
933            }
934            FileContent::File(_) => Err(FsError::NotADirectory),
935        }
936        // Note: The inode will be automatically cleaned up when the Rc ref count reaches 0
937    }
938
939    /// Rename or move a file or directory
940    pub fn rename(&mut self, old_path: &str, new_path: &str) -> Result<(), FsError> {
941        debug!("rename: old_path={}, new_path={}", old_path, new_path);
942
943        if old_path.is_empty() || new_path.is_empty() {
944            error!("rename: empty path");
945            return Err(FsError::InvalidArgument);
946        }
947
948        let old_comps: alloc::vec::Vec<&str> = old_path
949            .trim_start_matches('/')
950            .split('/')
951            .filter(|s| !s.is_empty())
952            .collect();
953
954        let new_comps: alloc::vec::Vec<&str> = new_path
955            .trim_start_matches('/')
956            .split('/')
957            .filter(|s| !s.is_empty())
958            .collect();
959
960        if old_comps.is_empty() || new_comps.is_empty() {
961            // Cannot rename root directory
962            return Err(FsError::InvalidArgument);
963        }
964
965        // Same path is a no-op
966        if old_comps == new_comps {
967            return Ok(());
968        }
969
970        // Navigate to old parent directory
971        let root_inode = self
972            .inode_table
973            .get(&self.root_inode)
974            .ok_or(FsError::NotFound)?
975            .clone();
976        let mut old_parent = root_inode.clone();
977
978        for comp in old_comps.iter().take(old_comps.len() - 1) {
979            old_parent = self.find_inode(&old_parent, comp)?;
980            let inode = old_parent.borrow();
981            if matches!(inode.content, FileContent::File(_)) {
982                return Err(FsError::NotADirectory);
983            }
984        }
985
986        let old_name = old_comps[old_comps.len() - 1];
987
988        // Verify the source exists and get its type
989        let source_inode = self.find_inode(&old_parent, old_name)?;
990        let source_is_dir = {
991            let inode = source_inode.borrow();
992            inode.metadata.is_dir
993        };
994
995        // Navigate to new parent directory
996        let mut new_parent = root_inode;
997
998        for comp in new_comps.iter().take(new_comps.len() - 1) {
999            new_parent = self.find_inode(&new_parent, comp)?;
1000            let inode = new_parent.borrow();
1001            if matches!(inode.content, FileContent::File(_)) {
1002                return Err(FsError::NotADirectory);
1003            }
1004        }
1005
1006        let new_name = new_comps[new_comps.len() - 1];
1007
1008        // Check if destination exists and validate type compatibility
1009        if let Ok(dest_inode) = self.find_inode(&new_parent, new_name) {
1010            let dest = dest_inode.borrow();
1011            let dest_is_dir = dest.metadata.is_dir;
1012
1013            if source_is_dir && !dest_is_dir {
1014                return Err(FsError::NotADirectory);
1015            }
1016            if !source_is_dir && dest_is_dir {
1017                return Err(FsError::IsADirectory);
1018            }
1019            if dest_is_dir
1020                && matches!(&dest.content, FileContent::Dir(entries) if !entries.is_empty())
1021            {
1022                return Err(FsError::NotEmpty);
1023            }
1024        }
1025
1026        // Determine if same directory by comparing inode IDs
1027        let old_parent_id = old_parent.borrow().id;
1028        let new_parent_id = new_parent.borrow().id;
1029
1030        let timestamp = self.time_provider.now();
1031
1032        if old_parent_id == new_parent_id {
1033            // Same directory: single borrow_mut to avoid double-borrow panic
1034            let mut parent = old_parent.borrow_mut();
1035            if let FileContent::Dir(entries) = &mut parent.content {
1036                if let Some(inode_id) = entries.remove(old_name) {
1037                    entries.insert(alloc::string::String::from(new_name), inode_id);
1038                    parent.metadata.modified = timestamp;
1039                } else {
1040                    return Err(FsError::NotFound);
1041                }
1042            } else {
1043                return Err(FsError::NotADirectory);
1044            }
1045        } else {
1046            // Cross-directory: remove from old, then insert into new
1047            let inode_id = {
1048                let mut parent = old_parent.borrow_mut();
1049                if let FileContent::Dir(entries) = &mut parent.content {
1050                    let id = entries.remove(old_name).ok_or(FsError::NotFound)?;
1051                    parent.metadata.modified = timestamp;
1052                    id
1053                } else {
1054                    return Err(FsError::NotADirectory);
1055                }
1056            };
1057
1058            let mut parent = new_parent.borrow_mut();
1059            if let FileContent::Dir(entries) = &mut parent.content {
1060                entries.insert(alloc::string::String::from(new_name), inode_id);
1061                parent.metadata.modified = timestamp;
1062            } else {
1063                return Err(FsError::NotADirectory);
1064            }
1065        }
1066
1067        debug!("rename: {} -> {}", old_path, new_path);
1068        Ok(())
1069    }
1070}
1071
1072impl Default for Fs<MonotonicCounter> {
1073    fn default() -> Self {
1074        Self::new()
1075    }
1076}
1077
1078impl Fs<MonotonicCounter> {
1079    pub fn new() -> Self {
1080        Self::with_time_provider(MonotonicCounter::new())
1081    }
1082}