Skip to main content

fs_core/
fs.rs

1//! Filesystem implementation.
2//!
3//! A single source of truth used in two modes via the `thread-safe` feature:
4//!
5//! - `thread-safe` (host runtimes, requires `std`): backed by `DashMap` and
6//!   atomic counters, with `Arc<RwLock<Inode>>` references. The whole API is
7//!   `&self` so multiple wasmtime hosts can share one `Fs` across threads.
8//! - default (WASI components or no-std embedders): backed by
9//!   `RefCell<HashMap>` (or `RefCell<BTreeMap>` in no-std) with `Cell<u64>`
10//!   counters and `Rc<RefCell<Inode>>` references. The API is still `&self`
11//!   thanks to interior mutability.
12//!
13//! Storage differences are isolated to a thin set of accessor helpers
14//! (`fd_with`, `inode_get`, `allocate_*`, ...) near the top of the file.
15//! Every public method then has a single body that compiles for both
16//! flavours.
17
18#[cfg(not(feature = "std"))]
19use alloc::collections::BTreeMap;
20
21#[cfg(feature = "thread-safe")]
22use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
23#[cfg(feature = "thread-safe")]
24use std::sync::{Arc, RwLock};
25
26#[cfg(feature = "thread-safe")]
27use dashmap::DashMap;
28
29#[cfg(all(feature = "std", not(feature = "thread-safe")))]
30use std::cell::{Cell, RefCell};
31#[cfg(all(feature = "std", not(feature = "thread-safe")))]
32use std::collections::HashMap;
33#[cfg(all(feature = "std", not(feature = "thread-safe")))]
34use std::rc::Rc;
35
36#[cfg(not(feature = "std"))]
37use alloc::rc::Rc;
38#[cfg(not(feature = "std"))]
39use core::cell::{Cell, RefCell};
40
41use crate::error::FsError;
42use crate::handle::FileHandle;
43use crate::inode::{FileContent, Inode, Metadata};
44use crate::time::{MonotonicCounter, TimeProvider};
45use crate::types::*;
46
47// Import logging macros (no-ops when the `logging` feature is off).
48#[cfg(feature = "logging")]
49use crate::{debug, error, trace};
50
51// =============================================================================
52// Storage type aliases
53// =============================================================================
54
55/// A reference to an `Inode` shared between the table and any open handles.
56///
57/// Thread-safe builds use `Arc<RwLock<Inode>>` so multiple readers can hold
58/// a guard at once. Single-thread builds use `Rc<RefCell<Inode>>`.
59#[cfg(feature = "thread-safe")]
60pub type InodeRef = Arc<RwLock<Inode>>;
61#[cfg(not(feature = "thread-safe"))]
62pub type InodeRef = Rc<RefCell<Inode>>;
63
64/// Table from `Fd` to `FileHandle`. `DashMap` in thread-safe mode (lock-free
65/// concurrent access across shards), plain map under a `RefCell` otherwise.
66#[cfg(feature = "thread-safe")]
67type FdTable = DashMap<Fd, FileHandle>;
68#[cfg(all(feature = "std", not(feature = "thread-safe")))]
69type FdTable = RefCell<HashMap<Fd, FileHandle>>;
70#[cfg(not(feature = "std"))]
71type FdTable = RefCell<BTreeMap<Fd, FileHandle>>;
72
73/// Table from `InodeId` to `InodeRef`.
74#[cfg(feature = "thread-safe")]
75type InodeTable = DashMap<InodeId, InodeRef>;
76#[cfg(all(feature = "std", not(feature = "thread-safe")))]
77type InodeTable = RefCell<HashMap<InodeId, InodeRef>>;
78#[cfg(not(feature = "std"))]
79type InodeTable = RefCell<BTreeMap<InodeId, InodeRef>>;
80
81/// Counter for the next free inode ID.
82#[cfg(feature = "thread-safe")]
83type NextInodeCounter = AtomicU64;
84#[cfg(not(feature = "thread-safe"))]
85type NextInodeCounter = Cell<InodeId>;
86
87/// Counter for the next free file descriptor.
88#[cfg(feature = "thread-safe")]
89type NextFdCounter = AtomicU32;
90#[cfg(not(feature = "thread-safe"))]
91type NextFdCounter = Cell<Fd>;
92
93// =============================================================================
94// Inode access macros
95// =============================================================================
96
97/// Read-locks an inode and yields a guard that derefs to `&Inode`.
98macro_rules! inode_read {
99    ($inode:expr) => {{
100        #[cfg(feature = "thread-safe")]
101        {
102            $inode.read().unwrap()
103        }
104        #[cfg(not(feature = "thread-safe"))]
105        {
106            $inode.borrow()
107        }
108    }};
109}
110
111/// Write-locks an inode and yields a guard that derefs to `&mut Inode`.
112macro_rules! inode_write {
113    ($inode:expr) => {{
114        #[cfg(feature = "thread-safe")]
115        {
116            $inode.write().unwrap()
117        }
118        #[cfg(not(feature = "thread-safe"))]
119        {
120            $inode.borrow_mut()
121        }
122    }};
123}
124
125// =============================================================================
126// Fs struct
127// =============================================================================
128
129/// Main filesystem structure. See the module-level comment for an overview
130/// of the two storage flavours.
131pub struct Fs<T: TimeProvider = MonotonicCounter> {
132    pub(crate) next_inode: NextInodeCounter,
133    pub(crate) next_fd: NextFdCounter,
134    pub(crate) fd_table: FdTable,
135    pub(crate) inode_table: InodeTable,
136    pub(crate) root_inode: InodeId,
137    pub(crate) time_provider: T,
138}
139
140impl<T: TimeProvider> Fs<T> {
141    // -------------------------------------------------------------------------
142    // Construction / accessors
143    // -------------------------------------------------------------------------
144
145    /// Create a new inode reference suitable for insertion in `inode_table`.
146    pub(crate) fn new_inode_ref(inode: Inode) -> InodeRef {
147        #[cfg(feature = "thread-safe")]
148        {
149            Arc::new(RwLock::new(inode))
150        }
151        #[cfg(not(feature = "thread-safe"))]
152        {
153            Rc::new(RefCell::new(inode))
154        }
155    }
156
157    /// Build an empty filesystem with the supplied time provider. A root
158    /// directory is created automatically with timestamps populated by the
159    /// provider.
160    pub fn with_time_provider(time_provider: T) -> Self {
161        let timestamp = time_provider.now();
162        let mut root_inode = Inode::new_dir(0);
163        root_inode.metadata.created = timestamp;
164        root_inode.metadata.modified = timestamp;
165
166        let fs = Self::empty_with(time_provider, 0, 1);
167        fs.inode_insert(0, Self::new_inode_ref(root_inode));
168        fs
169    }
170
171    /// Build an empty Fs with no inodes and no fd entries. Used by snapshot
172    /// restoration, which then populates `inode_table` from the snapshot's
173    /// inode list.
174    pub(crate) fn empty_with(
175        time_provider: T,
176        root_inode: InodeId,
177        next_inode_seed: InodeId,
178    ) -> Self {
179        Self {
180            next_inode: new_inode_counter(next_inode_seed),
181            next_fd: new_fd_counter(3), // 0/1/2 reserved for stdin/out/err
182            fd_table: new_fd_table(),
183            inode_table: new_inode_table(),
184            root_inode,
185            time_provider,
186        }
187    }
188
189    fn allocate_inode(&self) -> InodeId {
190        #[cfg(feature = "thread-safe")]
191        {
192            self.next_inode.fetch_add(1, Ordering::Relaxed) as InodeId
193        }
194        #[cfg(not(feature = "thread-safe"))]
195        {
196            let id = self.next_inode.get();
197            self.next_inode.set(id + 1);
198            id
199        }
200    }
201
202    fn allocate_fd(&self) -> Fd {
203        #[cfg(feature = "thread-safe")]
204        {
205            self.next_fd.fetch_add(1, Ordering::Relaxed)
206        }
207        #[cfg(not(feature = "thread-safe"))]
208        {
209            let fd = self.next_fd.get();
210            self.next_fd.set(fd + 1);
211            fd
212        }
213    }
214
215    // -------------------------------------------------------------------------
216    // fd_table helpers
217    // -------------------------------------------------------------------------
218
219    /// Run `f` with access to the `FileHandle` for `fd`. Returns
220    /// `BadFileDescriptor` if the fd is unknown.
221    ///
222    /// The closure holds the underlying borrow / shard lock for its full
223    /// duration, so it should not call back into `Fs` methods that mutate
224    /// the same table (this would `panic!` under `RefCell` and could
225    /// deadlock under `DashMap`).
226    fn fd_with<R>(&self, fd: Fd, f: impl FnOnce(&FileHandle) -> R) -> Result<R, FsError> {
227        #[cfg(feature = "thread-safe")]
228        {
229            let handle = self.fd_table.get(&fd).ok_or(FsError::BadFileDescriptor)?;
230            Ok(f(&handle))
231        }
232        #[cfg(not(feature = "thread-safe"))]
233        {
234            let map = self.fd_table.borrow();
235            let handle = map.get(&fd).ok_or(FsError::BadFileDescriptor)?;
236            Ok(f(handle))
237        }
238    }
239
240    /// Like `fd_with` but returns `None` (not an error) when the fd is
241    /// missing. Useful for "best effort" position updates.
242    fn fd_try_with<R>(&self, fd: Fd, f: impl FnOnce(&FileHandle) -> R) -> Option<R> {
243        #[cfg(feature = "thread-safe")]
244        {
245            self.fd_table.get(&fd).map(|h| f(&h))
246        }
247        #[cfg(not(feature = "thread-safe"))]
248        {
249            self.fd_table.borrow().get(&fd).map(f)
250        }
251    }
252
253    fn fd_insert(&self, fd: Fd, handle: FileHandle) {
254        #[cfg(feature = "thread-safe")]
255        {
256            self.fd_table.insert(fd, handle);
257        }
258        #[cfg(not(feature = "thread-safe"))]
259        {
260            self.fd_table.borrow_mut().insert(fd, handle);
261        }
262    }
263
264    fn fd_remove(&self, fd: Fd) -> Option<FileHandle> {
265        #[cfg(feature = "thread-safe")]
266        {
267            self.fd_table.remove(&fd).map(|(_, v)| v)
268        }
269        #[cfg(not(feature = "thread-safe"))]
270        {
271            self.fd_table.borrow_mut().remove(&fd)
272        }
273    }
274
275    // -------------------------------------------------------------------------
276    // inode_table helpers
277    // -------------------------------------------------------------------------
278
279    /// Look up an inode by id. Returns a clone of the (cheap) `Arc`/`Rc`
280    /// reference rather than borrowing the table, so callers don't need to
281    /// worry about borrow lifetimes.
282    pub(crate) fn inode_get(&self, id: InodeId) -> Option<InodeRef> {
283        #[cfg(feature = "thread-safe")]
284        {
285            self.inode_table.get(&id).map(|r| r.clone())
286        }
287        #[cfg(not(feature = "thread-safe"))]
288        {
289            self.inode_table.borrow().get(&id).cloned()
290        }
291    }
292
293    pub(crate) fn inode_insert(&self, id: InodeId, inode: InodeRef) {
294        #[cfg(feature = "thread-safe")]
295        {
296            self.inode_table.insert(id, inode);
297        }
298        #[cfg(not(feature = "thread-safe"))]
299        {
300            self.inode_table.borrow_mut().insert(id, inode);
301        }
302    }
303
304    /// Visit every inode in the table. Used by snapshot serialization.
305    /// Holds the inode table lock for the duration of iteration.
306    pub(crate) fn inode_for_each<F>(&self, mut f: F)
307    where
308        F: FnMut(InodeId, &InodeRef),
309    {
310        #[cfg(feature = "thread-safe")]
311        {
312            for entry in self.inode_table.iter() {
313                f(*entry.key(), entry.value());
314            }
315        }
316        #[cfg(not(feature = "thread-safe"))]
317        {
318            for (&id, inode_rc) in self.inode_table.borrow().iter() {
319                f(id, inode_rc);
320            }
321        }
322    }
323
324    /// Current value of the next-inode counter (for snapshot serialization).
325    pub(crate) fn next_inode_value(&self) -> InodeId {
326        #[cfg(feature = "thread-safe")]
327        {
328            self.next_inode.load(Ordering::Relaxed)
329        }
330        #[cfg(not(feature = "thread-safe"))]
331        {
332            self.next_inode.get()
333        }
334    }
335
336    // -------------------------------------------------------------------------
337    // Internal path / inode helpers
338    // -------------------------------------------------------------------------
339
340    fn find_inode(&self, parent_inode: &InodeRef, name: &str) -> Result<InodeRef, FsError> {
341        let parent = inode_read!(parent_inode);
342
343        match &parent.content {
344            FileContent::Dir(entries) => {
345                if let Some(&inode_id) = entries.get(name) {
346                    self.inode_get(inode_id).ok_or(FsError::NotFound)
347                } else {
348                    Err(FsError::NotFound)
349                }
350            }
351            FileContent::File(_) => Err(FsError::NotADirectory),
352        }
353    }
354
355    fn create_inode(
356        &self,
357        parent_inode: &InodeRef,
358        name: &str,
359        is_dir: bool,
360    ) -> Result<InodeRef, FsError> {
361        let mut parent = inode_write!(parent_inode);
362
363        match &mut parent.content {
364            FileContent::Dir(entries) => {
365                if entries.contains_key(name) {
366                    return Err(FsError::AlreadyExists);
367                }
368
369                let new_inode_id = self.allocate_inode();
370                let timestamp = self.time_provider.now();
371                let mut new_inode = if is_dir {
372                    Inode::new_dir(new_inode_id)
373                } else {
374                    Inode::new_file(new_inode_id)
375                };
376
377                new_inode.metadata.created = timestamp;
378                new_inode.metadata.modified = timestamp;
379
380                let new_inode_ref = Self::new_inode_ref(new_inode);
381                entries.insert(name.into(), new_inode_id);
382                self.inode_insert(new_inode_id, new_inode_ref.clone());
383                parent.metadata.modified = timestamp;
384
385                Ok(new_inode_ref)
386            }
387            FileContent::File(_) => Err(FsError::NotADirectory),
388        }
389    }
390
391    // -------------------------------------------------------------------------
392    // Public API
393    // -------------------------------------------------------------------------
394
395    pub fn open_path(&self, path: &str) -> Result<Fd, FsError> {
396        self.open_path_with_flags(path, O_RDWR | O_CREAT)
397    }
398
399    pub fn open_at(&self, dir_fd: Fd, path: &str, flags: u32) -> Result<Fd, FsError> {
400        debug!(
401            "open_at: dir_fd={}, path={}, flags={:#x}",
402            dir_fd, path, flags
403        );
404
405        if path.starts_with('/') {
406            error!("open_at: absolute path not allowed");
407            return Err(FsError::InvalidArgument);
408        }
409
410        let dir_inode_id = self.fd_with(dir_fd, |h| h.inode_id).map_err(|_| {
411            error!("open_at: bad directory file descriptor {}", dir_fd);
412            FsError::BadFileDescriptor
413        })?;
414
415        let dir_inode = self.inode_get(dir_inode_id).ok_or(FsError::NotFound)?;
416
417        // Verify it's a directory
418        {
419            let inode = inode_read!(dir_inode);
420            if matches!(inode.content, FileContent::File(_)) {
421                error!("open_at: dir_fd {} is not a directory", dir_fd);
422                return Err(FsError::NotADirectory);
423            }
424        }
425
426        if path.is_empty() {
427            error!("open_at: empty path not supported yet");
428            return Err(FsError::InvalidArgument);
429        }
430
431        let comps: alloc::vec::Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
432
433        if comps.is_empty() {
434            return Err(FsError::InvalidArgument);
435        }
436
437        let mut current_inode = dir_inode;
438
439        for comp in comps.iter().take(comps.len() - 1) {
440            current_inode = self.find_inode(&current_inode, comp)?;
441            let inode = inode_read!(current_inode);
442            if matches!(inode.content, FileContent::File(_)) {
443                return Err(FsError::NotADirectory);
444            }
445        }
446
447        let filename = comps[comps.len() - 1];
448        let file_inode = match self.find_inode(&current_inode, filename) {
449            Ok(inode) => inode,
450            Err(FsError::NotFound) if flags & O_CREAT != 0 => {
451                self.create_inode(&current_inode, filename, false)?
452            }
453            Err(e) => return Err(e),
454        };
455
456        // POSIX: opening a directory with write access or O_TRUNC is not allowed.
457        {
458            let inode = inode_read!(file_inode);
459            if matches!(inode.content, FileContent::Dir(_)) {
460                let access_mode = flags & 0x3;
461                if access_mode == O_WRONLY || access_mode == O_RDWR {
462                    return Err(FsError::IsADirectory);
463                }
464                if flags & O_TRUNC != 0 {
465                    return Err(FsError::IsADirectory);
466                }
467            }
468        }
469
470        // O_TRUNC: truncate file to 0 bytes (POSIX requires write permission)
471        if flags & O_TRUNC != 0 {
472            let access_mode = flags & 0x3;
473            if access_mode == O_RDONLY {
474                return Err(FsError::InvalidArgument);
475            }
476            let mut inode = inode_write!(file_inode);
477            if let FileContent::File(storage) = &mut inode.content {
478                storage.truncate(0);
479                inode.metadata.size = 0;
480                inode.metadata.modified = self.time_provider.now();
481            }
482        }
483
484        let inode_id = inode_read!(file_inode).id;
485        let handle = FileHandle::new(inode_id, 0, flags);
486
487        let fd = self.allocate_fd();
488        self.fd_insert(fd, handle);
489        debug!("open_at: allocated fd={} for inode={}", fd, inode_id);
490        Ok(fd)
491    }
492
493    pub fn open_path_with_flags(&self, path: &str, flags: u32) -> Result<Fd, FsError> {
494        debug!("open_path_with_flags: path={}, flags={:#x}", path, flags);
495
496        if path.is_empty() {
497            error!("open_path_with_flags: empty path");
498            return Err(FsError::InvalidArgument);
499        }
500
501        let comps: alloc::vec::Vec<&str> = path
502            .trim_start_matches('/')
503            .split('/')
504            .filter(|s| !s.is_empty())
505            .collect();
506
507        // Special case: opening root directory "/"
508        if comps.is_empty() {
509            debug!("open_path_with_flags: opening root directory");
510            let handle = FileHandle::new(self.root_inode, 0, flags);
511            let fd = self.allocate_fd();
512            self.fd_insert(fd, handle);
513            debug!("open_path_with_flags: allocated fd={} for root", fd);
514            return Ok(fd);
515        }
516
517        let root_inode = self.inode_get(self.root_inode).ok_or(FsError::NotFound)?;
518        let mut current_inode = root_inode;
519
520        for comp in comps.iter().take(comps.len() - 1) {
521            current_inode = self.find_inode(&current_inode, comp)?;
522            let inode = inode_read!(current_inode);
523            if matches!(inode.content, FileContent::File(_)) {
524                return Err(FsError::NotADirectory);
525            }
526        }
527
528        let filename = comps[comps.len() - 1];
529        let file_inode = match self.find_inode(&current_inode, filename) {
530            Ok(inode) => inode,
531            Err(FsError::NotFound) if flags & O_CREAT != 0 => {
532                self.create_inode(&current_inode, filename, false)?
533            }
534            Err(e) => return Err(e),
535        };
536
537        {
538            let inode = inode_read!(file_inode);
539            if matches!(inode.content, FileContent::Dir(_)) {
540                let access_mode = flags & 0x3;
541                if access_mode == O_WRONLY || access_mode == O_RDWR {
542                    return Err(FsError::IsADirectory);
543                }
544                if flags & O_TRUNC != 0 {
545                    return Err(FsError::IsADirectory);
546                }
547            }
548        }
549
550        if flags & O_TRUNC != 0 {
551            let access_mode = flags & 0x3;
552            if access_mode == O_RDONLY {
553                return Err(FsError::InvalidArgument);
554            }
555            let mut inode = inode_write!(file_inode);
556            if let FileContent::File(storage) = &mut inode.content {
557                storage.truncate(0);
558                inode.metadata.size = 0;
559                inode.metadata.modified = self.time_provider.now();
560            }
561        }
562
563        let inode_id = inode_read!(file_inode).id;
564        let handle = FileHandle::new(inode_id, 0, flags);
565
566        let fd = self.allocate_fd();
567        self.fd_insert(fd, handle);
568        debug!(
569            "open_path_with_flags: allocated fd={} for inode={}",
570            fd, inode_id
571        );
572        Ok(fd)
573    }
574
575    pub fn write(&self, fd: Fd, buf: &[u8]) -> Result<usize, FsError> {
576        trace!("write: fd={}, len={}", fd, buf.len());
577
578        let (inode_id, flags, position) = self
579            .fd_with(fd, |h| (h.inode_id, h.flags, h.get_position()))
580            .map_err(|_| {
581                error!("write: bad file descriptor {}", fd);
582                FsError::BadFileDescriptor
583            })?;
584
585        let access_mode = flags & 0x3;
586        if access_mode == O_RDONLY {
587            return Err(FsError::PermissionDenied);
588        }
589
590        let inode = self.inode_get(inode_id).ok_or(FsError::NotFound)?;
591        let mut inode_ref = inode_write!(inode);
592
593        match &mut inode_ref.content {
594            FileContent::File(storage) => {
595                // O_APPEND: write at end of file
596                let pos = if flags & O_APPEND != 0 {
597                    storage.size()
598                } else {
599                    position as usize
600                };
601
602                let n = storage.write(pos, buf);
603                inode_ref.metadata.size = storage.size() as u64;
604                inode_ref.metadata.modified = self.time_provider.now();
605
606                // Release inode lock before re-acquiring the fd entry.
607                drop(inode_ref);
608                self.fd_try_with(fd, |h| h.set_position((pos + n) as u64));
609
610                debug!("write: fd={}, wrote {} bytes at pos {}", fd, n, pos);
611                Ok(n)
612            }
613            FileContent::Dir(_) => {
614                error!("write: fd={} is a directory", fd);
615                Err(FsError::BadFileDescriptor)
616            }
617        }
618    }
619
620    /// Write data at a specific offset atomically (seek + write in one
621    /// lock acquisition). Does not touch O_APPEND.
622    pub fn write_at(&self, fd: Fd, offset: u64, buf: &[u8]) -> Result<usize, FsError> {
623        trace!("write_at: fd={}, offset={}, len={}", fd, offset, buf.len());
624
625        let (inode_id, flags) = self.fd_with(fd, |h| (h.inode_id, h.flags)).map_err(|_| {
626            error!("write_at: bad file descriptor {}", fd);
627            FsError::BadFileDescriptor
628        })?;
629
630        let access_mode = flags & 0x3;
631        if access_mode == O_RDONLY {
632            return Err(FsError::PermissionDenied);
633        }
634
635        let inode = self.inode_get(inode_id).ok_or(FsError::NotFound)?;
636        let mut inode_ref = inode_write!(inode);
637
638        match &mut inode_ref.content {
639            FileContent::File(storage) => {
640                let n = storage.write(offset as usize, buf);
641                inode_ref.metadata.size = storage.size() as u64;
642                inode_ref.metadata.modified = self.time_provider.now();
643
644                drop(inode_ref);
645                self.fd_try_with(fd, |h| h.set_position(offset + n as u64));
646
647                debug!(
648                    "write_at: fd={}, wrote {} bytes at offset {}",
649                    fd, n, offset
650                );
651                Ok(n)
652            }
653            FileContent::Dir(_) => {
654                error!("write_at: fd={} is a directory", fd);
655                Err(FsError::BadFileDescriptor)
656            }
657        }
658    }
659
660    /// Append data to file atomically (always writes at end of file).
661    pub fn append_write(&self, fd: Fd, buf: &[u8]) -> Result<usize, FsError> {
662        trace!("append_write: fd={}, len={}", fd, buf.len());
663
664        let (inode_id, flags) = self.fd_with(fd, |h| (h.inode_id, h.flags)).map_err(|_| {
665            error!("append_write: bad file descriptor {}", fd);
666            FsError::BadFileDescriptor
667        })?;
668
669        let access_mode = flags & 0x3;
670        if access_mode == O_RDONLY {
671            return Err(FsError::PermissionDenied);
672        }
673
674        let inode = self.inode_get(inode_id).ok_or(FsError::NotFound)?;
675        let mut inode_ref = inode_write!(inode);
676
677        match &mut inode_ref.content {
678            FileContent::File(storage) => {
679                let pos = storage.size();
680                let n = storage.write(pos, buf);
681                inode_ref.metadata.size = storage.size() as u64;
682                inode_ref.metadata.modified = self.time_provider.now();
683
684                drop(inode_ref);
685                self.fd_try_with(fd, |h| h.set_position((pos + n) as u64));
686
687                debug!(
688                    "append_write: fd={}, appended {} bytes at pos {}",
689                    fd, n, pos
690                );
691                Ok(n)
692            }
693            FileContent::Dir(_) => {
694                error!("append_write: fd={} is a directory", fd);
695                Err(FsError::BadFileDescriptor)
696            }
697        }
698    }
699
700    pub fn read(&self, fd: Fd, out: &mut [u8]) -> Result<usize, FsError> {
701        trace!("read: fd={}, buf_len={}", fd, out.len());
702
703        let (inode_id, flags, position) = self
704            .fd_with(fd, |h| (h.inode_id, h.flags, h.get_position()))
705            .map_err(|_| {
706                error!("read: bad file descriptor {}", fd);
707                FsError::BadFileDescriptor
708            })?;
709
710        let access_mode = flags & 0x3;
711        if access_mode == O_WRONLY {
712            return Err(FsError::PermissionDenied);
713        }
714
715        let inode = self.inode_get(inode_id).ok_or(FsError::NotFound)?;
716        let inode_ref = inode_read!(inode);
717
718        match &inode_ref.content {
719            FileContent::File(storage) => {
720                let pos = position as usize;
721                let n = storage.read(pos, out);
722                drop(inode_ref);
723                self.fd_try_with(fd, |h| h.set_position(position + n as u64));
724                debug!("read: fd={}, read {} bytes at pos {}", fd, n, pos);
725                Ok(n)
726            }
727            FileContent::Dir(_) => {
728                error!("read: fd={} is a directory", fd);
729                Err(FsError::BadFileDescriptor)
730            }
731        }
732    }
733
734    /// Read data at a specific offset atomically (seek + read in one
735    /// lock acquisition).
736    pub fn read_at(&self, fd: Fd, offset: u64, out: &mut [u8]) -> Result<usize, FsError> {
737        trace!(
738            "read_at: fd={}, offset={}, buf_len={}",
739            fd,
740            offset,
741            out.len()
742        );
743
744        let (inode_id, flags) = self.fd_with(fd, |h| (h.inode_id, h.flags)).map_err(|_| {
745            error!("read_at: bad file descriptor {}", fd);
746            FsError::BadFileDescriptor
747        })?;
748
749        let access_mode = flags & 0x3;
750        if access_mode == O_WRONLY {
751            return Err(FsError::PermissionDenied);
752        }
753
754        let inode = self.inode_get(inode_id).ok_or(FsError::NotFound)?;
755        let inode_ref = inode_read!(inode);
756
757        match &inode_ref.content {
758            FileContent::File(storage) => {
759                let n = storage.read(offset as usize, out);
760                drop(inode_ref);
761                self.fd_try_with(fd, |h| h.set_position(offset + n as u64));
762                debug!("read_at: fd={}, read {} bytes at offset {}", fd, n, offset);
763                Ok(n)
764            }
765            FileContent::Dir(_) => {
766                error!("read_at: fd={} is a directory", fd);
767                Err(FsError::BadFileDescriptor)
768            }
769        }
770    }
771
772    pub fn ftruncate(&self, fd: Fd, size: u64) -> Result<(), FsError> {
773        let (inode_id, flags) = self.fd_with(fd, |h| (h.inode_id, h.flags))?;
774
775        let access_mode = flags & 0x3;
776        if access_mode == O_RDONLY {
777            return Err(FsError::PermissionDenied);
778        }
779
780        let inode = self.inode_get(inode_id).ok_or(FsError::NotFound)?;
781        let mut inode_ref = inode_write!(inode);
782
783        match &mut inode_ref.content {
784            FileContent::File(storage) => {
785                storage.truncate(size as usize);
786                inode_ref.metadata.size = size;
787                inode_ref.metadata.modified = self.time_provider.now();
788                Ok(())
789            }
790            FileContent::Dir(_) => Err(FsError::BadFileDescriptor),
791        }
792    }
793
794    pub fn close(&self, fd: Fd) -> Result<(), FsError> {
795        trace!("close: fd={}", fd);
796
797        self.fd_remove(fd).ok_or_else(|| {
798            error!("close: bad file descriptor {}", fd);
799            FsError::BadFileDescriptor
800        })?;
801
802        debug!("close: fd={} closed successfully", fd);
803        Ok(())
804    }
805
806    pub fn stat(&self, path: &str) -> Result<Metadata, FsError> {
807        if path.is_empty() {
808            return Err(FsError::InvalidArgument);
809        }
810
811        let comps: alloc::vec::Vec<&str> = path
812            .trim_start_matches('/')
813            .split('/')
814            .filter(|s| !s.is_empty())
815            .collect();
816
817        if comps.is_empty() {
818            let root = self.inode_get(self.root_inode).ok_or(FsError::NotFound)?;
819            return Ok(inode_read!(root).metadata);
820        }
821
822        let mut current_inode = self.inode_get(self.root_inode).ok_or(FsError::NotFound)?;
823
824        for comp in comps.iter() {
825            current_inode = self.find_inode(&current_inode, comp)?;
826        }
827
828        Ok(inode_read!(current_inode).metadata)
829    }
830
831    pub fn fstat(&self, fd: Fd) -> Result<Metadata, FsError> {
832        let inode_id = self.fd_with(fd, |h| h.inode_id)?;
833        let inode = self.inode_get(inode_id).ok_or(FsError::NotFound)?;
834        Ok(inode_read!(inode).metadata)
835    }
836
837    pub fn seek(&self, fd: Fd, offset: i64, whence: i32) -> Result<u64, FsError> {
838        trace!("seek: fd={}, offset={}, whence={}", fd, offset, whence);
839
840        const SEEK_SET: i32 = 0;
841        const SEEK_CUR: i32 = 1;
842        const SEEK_END: i32 = 2;
843
844        let (inode_id, current) = self
845            .fd_with(fd, |h| (h.inode_id, h.get_position()))
846            .map_err(|_| {
847                error!("seek: bad file descriptor {}", fd);
848                FsError::BadFileDescriptor
849            })?;
850        let inode = self.inode_get(inode_id).ok_or(FsError::NotFound)?;
851        let inode_ref = inode_read!(inode);
852
853        match &inode_ref.content {
854            FileContent::File(storage) => {
855                let new_pos = match whence {
856                    SEEK_SET => {
857                        if offset < 0 {
858                            return Err(FsError::InvalidArgument);
859                        }
860                        offset as u64
861                    }
862                    SEEK_CUR => {
863                        let new = current as i64 + offset;
864                        if new < 0 {
865                            return Err(FsError::InvalidArgument);
866                        }
867                        new as u64
868                    }
869                    SEEK_END => {
870                        let size = storage.size() as i64;
871                        let new = size + offset;
872                        if new < 0 {
873                            return Err(FsError::InvalidArgument);
874                        }
875                        new as u64
876                    }
877                    _ => return Err(FsError::InvalidArgument),
878                };
879
880                drop(inode_ref);
881                self.fd_try_with(fd, |h| h.set_position(new_pos));
882                debug!("seek: fd={}, new_pos={}", fd, new_pos);
883                Ok(new_pos)
884            }
885            FileContent::Dir(_) => Err(FsError::BadFileDescriptor),
886        }
887    }
888
889    pub fn mkdir(&self, path: &str) -> Result<(), FsError> {
890        debug!("mkdir: path={}", path);
891
892        if path.is_empty() {
893            error!("mkdir: empty path");
894            return Err(FsError::InvalidArgument);
895        }
896
897        let comps: alloc::vec::Vec<&str> = path
898            .trim_start_matches('/')
899            .split('/')
900            .filter(|s| !s.is_empty())
901            .collect();
902
903        if comps.is_empty() {
904            return Err(FsError::InvalidArgument);
905        }
906
907        let mut current_inode = self.inode_get(self.root_inode).ok_or(FsError::NotFound)?;
908
909        for comp in comps.iter().take(comps.len() - 1) {
910            current_inode = self.find_inode(&current_inode, comp)?;
911            let inode = inode_read!(current_inode);
912            if matches!(inode.content, FileContent::File(_)) {
913                return Err(FsError::NotADirectory);
914            }
915        }
916
917        let dirname = comps[comps.len() - 1];
918        self.create_inode(&current_inode, dirname, true)?;
919        debug!("mkdir: created directory {}", path);
920        Ok(())
921    }
922
923    pub fn mkdir_p(&self, path: &str) -> Result<(), FsError> {
924        if path.is_empty() {
925            return Err(FsError::InvalidArgument);
926        }
927
928        let comps: alloc::vec::Vec<&str> = path
929            .trim_start_matches('/')
930            .split('/')
931            .filter(|s| !s.is_empty())
932            .collect();
933
934        if comps.is_empty() {
935            return Ok(()); // Root already exists
936        }
937
938        let mut current_inode = self.inode_get(self.root_inode).ok_or(FsError::NotFound)?;
939
940        for comp in comps.iter() {
941            current_inode = match self.find_inode(&current_inode, comp) {
942                Ok(inode) => {
943                    {
944                        let borrowed = inode_read!(inode);
945                        if matches!(borrowed.content, FileContent::File(_)) {
946                            return Err(FsError::NotADirectory);
947                        }
948                    }
949                    inode
950                }
951                Err(FsError::NotFound) => self.create_inode(&current_inode, comp, true)?,
952                Err(e) => return Err(e),
953            };
954        }
955
956        Ok(())
957    }
958
959    pub fn unlink(&self, path: &str) -> Result<(), FsError> {
960        debug!("unlink: path={}", path);
961
962        if path.is_empty() {
963            error!("unlink: empty path");
964            return Err(FsError::InvalidArgument);
965        }
966
967        let comps: alloc::vec::Vec<&str> = path
968            .trim_start_matches('/')
969            .split('/')
970            .filter(|s| !s.is_empty())
971            .collect();
972
973        if comps.is_empty() {
974            return Err(FsError::InvalidArgument);
975        }
976
977        let mut current_inode = self.inode_get(self.root_inode).ok_or(FsError::NotFound)?;
978
979        for comp in comps.iter().take(comps.len() - 1) {
980            current_inode = self.find_inode(&current_inode, comp)?;
981            let inode = inode_read!(current_inode);
982            if matches!(inode.content, FileContent::File(_)) {
983                return Err(FsError::NotADirectory);
984            }
985        }
986
987        let filename = comps[comps.len() - 1];
988
989        // Verify the target exists and is not a directory.
990        let target_inode = self.find_inode(&current_inode, filename)?;
991        {
992            let target = inode_read!(target_inode);
993            if matches!(target.content, FileContent::Dir(_)) {
994                return Err(FsError::IsADirectory);
995            }
996        }
997
998        // Remove the entry from the parent directory.
999        let mut parent = inode_write!(current_inode);
1000        match &mut parent.content {
1001            FileContent::Dir(entries) => {
1002                entries.remove(filename);
1003                let timestamp = self.time_provider.now();
1004                parent.metadata.modified = timestamp;
1005                debug!("unlink: removed {}", path);
1006                Ok(())
1007            }
1008            FileContent::File(_) => Err(FsError::NotADirectory),
1009        }
1010        // The inode is cleaned up when the last `Arc`/`Rc` ref drops.
1011    }
1012
1013    pub fn readdir(&self, path: &str) -> Result<alloc::vec::Vec<alloc::string::String>, FsError> {
1014        if path.is_empty() {
1015            return Err(FsError::InvalidArgument);
1016        }
1017
1018        let comps: alloc::vec::Vec<&str> = path
1019            .trim_start_matches('/')
1020            .split('/')
1021            .filter(|s| !s.is_empty())
1022            .collect();
1023
1024        let mut current_inode = self.inode_get(self.root_inode).ok_or(FsError::NotFound)?;
1025
1026        for comp in comps.iter() {
1027            current_inode = self.find_inode(&current_inode, comp)?;
1028        }
1029
1030        let inode = inode_read!(current_inode);
1031        match &inode.content {
1032            FileContent::Dir(entries) => {
1033                let mut result = alloc::vec::Vec::new();
1034                for name in entries.keys() {
1035                    result.push(name.clone());
1036                }
1037                Ok(result)
1038            }
1039            FileContent::File(_) => Err(FsError::NotADirectory),
1040        }
1041    }
1042
1043    /// Read directory entries from an open file descriptor.
1044    /// Returns a list of `(name, is_dir)` tuples.
1045    pub fn readdir_fd(
1046        &self,
1047        fd: Fd,
1048    ) -> Result<alloc::vec::Vec<(alloc::string::String, bool)>, FsError> {
1049        let inode_id = self.fd_with(fd, |h| h.inode_id)?;
1050        let inode = self.inode_get(inode_id).ok_or(FsError::NotFound)?;
1051
1052        // Snapshot the (name, child_id) pairs so we don't hold the inode
1053        // lock while looking up child inodes (which would conflict with a
1054        // hypothetical concurrent writer holding both).
1055        let entries: alloc::vec::Vec<(alloc::string::String, InodeId)> = {
1056            let inode_ref = inode_read!(inode);
1057            match &inode_ref.content {
1058                FileContent::Dir(map) => map.iter().map(|(n, &id)| (n.clone(), id)).collect(),
1059                FileContent::File(_) => return Err(FsError::NotADirectory),
1060            }
1061        };
1062
1063        let mut result = alloc::vec::Vec::with_capacity(entries.len());
1064        for (name, child_id) in entries {
1065            if let Some(child_inode) = self.inode_get(child_id) {
1066                let is_dir = inode_read!(child_inode).metadata.is_dir;
1067                result.push((name, is_dir));
1068            }
1069        }
1070        Ok(result)
1071    }
1072
1073    /// Remove an empty directory.
1074    pub fn rmdir(&self, path: &str) -> Result<(), FsError> {
1075        debug!("rmdir: path={}", path);
1076
1077        if path.is_empty() {
1078            error!("rmdir: empty path");
1079            return Err(FsError::InvalidArgument);
1080        }
1081
1082        let comps: alloc::vec::Vec<&str> = path
1083            .trim_start_matches('/')
1084            .split('/')
1085            .filter(|s| !s.is_empty())
1086            .collect();
1087
1088        if comps.is_empty() {
1089            return Err(FsError::InvalidArgument);
1090        }
1091
1092        let mut current_inode = self.inode_get(self.root_inode).ok_or(FsError::NotFound)?;
1093
1094        for comp in comps.iter().take(comps.len() - 1) {
1095            current_inode = self.find_inode(&current_inode, comp)?;
1096            let inode = inode_read!(current_inode);
1097            if matches!(inode.content, FileContent::File(_)) {
1098                return Err(FsError::NotADirectory);
1099            }
1100        }
1101
1102        let dirname = comps[comps.len() - 1];
1103        let target_inode = self.find_inode(&current_inode, dirname)?;
1104
1105        {
1106            let target = inode_read!(target_inode);
1107            match &target.content {
1108                FileContent::File(_) => return Err(FsError::NotADirectory),
1109                FileContent::Dir(entries) => {
1110                    if !entries.is_empty() {
1111                        error!("rmdir: directory not empty");
1112                        return Err(FsError::NotEmpty);
1113                    }
1114                }
1115            }
1116        }
1117
1118        let mut parent = inode_write!(current_inode);
1119        match &mut parent.content {
1120            FileContent::Dir(entries) => {
1121                entries.remove(dirname);
1122                let timestamp = self.time_provider.now();
1123                parent.metadata.modified = timestamp;
1124                debug!("rmdir: removed {}", path);
1125                Ok(())
1126            }
1127            FileContent::File(_) => Err(FsError::NotADirectory),
1128        }
1129    }
1130
1131    /// Rename or move a file or directory.
1132    pub fn rename(&self, old_path: &str, new_path: &str) -> Result<(), FsError> {
1133        debug!("rename: old_path={}, new_path={}", old_path, new_path);
1134
1135        if old_path.is_empty() || new_path.is_empty() {
1136            error!("rename: empty path");
1137            return Err(FsError::InvalidArgument);
1138        }
1139
1140        let old_comps: alloc::vec::Vec<&str> = old_path
1141            .trim_start_matches('/')
1142            .split('/')
1143            .filter(|s| !s.is_empty())
1144            .collect();
1145
1146        let new_comps: alloc::vec::Vec<&str> = new_path
1147            .trim_start_matches('/')
1148            .split('/')
1149            .filter(|s| !s.is_empty())
1150            .collect();
1151
1152        if old_comps.is_empty() || new_comps.is_empty() {
1153            return Err(FsError::InvalidArgument);
1154        }
1155
1156        if old_comps == new_comps {
1157            return Ok(());
1158        }
1159
1160        // Navigate to old parent directory.
1161        let root_inode = self.inode_get(self.root_inode).ok_or(FsError::NotFound)?;
1162        let mut old_parent = root_inode.clone();
1163
1164        for comp in old_comps.iter().take(old_comps.len() - 1) {
1165            old_parent = self.find_inode(&old_parent, comp)?;
1166            let inode = inode_read!(old_parent);
1167            if matches!(inode.content, FileContent::File(_)) {
1168                return Err(FsError::NotADirectory);
1169            }
1170        }
1171
1172        let old_name = old_comps[old_comps.len() - 1];
1173
1174        // Verify source exists and get its type.
1175        let source_inode = self.find_inode(&old_parent, old_name)?;
1176        let source_is_dir = inode_read!(source_inode).metadata.is_dir;
1177
1178        // Navigate to new parent directory.
1179        let mut new_parent = root_inode;
1180
1181        for comp in new_comps.iter().take(new_comps.len() - 1) {
1182            new_parent = self.find_inode(&new_parent, comp)?;
1183            let inode = inode_read!(new_parent);
1184            if matches!(inode.content, FileContent::File(_)) {
1185                return Err(FsError::NotADirectory);
1186            }
1187        }
1188
1189        let new_name = new_comps[new_comps.len() - 1];
1190
1191        // If destination exists, validate type compatibility.
1192        if let Ok(dest_inode) = self.find_inode(&new_parent, new_name) {
1193            let dest = inode_read!(dest_inode);
1194            let dest_is_dir = dest.metadata.is_dir;
1195
1196            if source_is_dir && !dest_is_dir {
1197                return Err(FsError::NotADirectory);
1198            }
1199            if !source_is_dir && dest_is_dir {
1200                return Err(FsError::IsADirectory);
1201            }
1202            if dest_is_dir
1203                && matches!(&dest.content, FileContent::Dir(entries) if !entries.is_empty())
1204            {
1205                return Err(FsError::NotEmpty);
1206            }
1207        }
1208
1209        let old_parent_id = inode_read!(old_parent).id;
1210        let new_parent_id = inode_read!(new_parent).id;
1211
1212        let timestamp = self.time_provider.now();
1213
1214        if old_parent_id == new_parent_id {
1215            // Same directory: single lock, remove + insert.
1216            let mut parent = inode_write!(old_parent);
1217            if let FileContent::Dir(entries) = &mut parent.content {
1218                if let Some(inode_id) = entries.remove(old_name) {
1219                    entries.insert(alloc::string::String::from(new_name), inode_id);
1220                    parent.metadata.modified = timestamp;
1221                } else {
1222                    return Err(FsError::NotFound);
1223                }
1224            } else {
1225                return Err(FsError::NotADirectory);
1226            }
1227        } else {
1228            // Cross-directory: lock both parents in inode id order to avoid
1229            // deadlock under thread-safe builds.
1230            if old_parent_id < new_parent_id {
1231                let mut old_p = inode_write!(old_parent);
1232                let mut new_p = inode_write!(new_parent);
1233                let old_entries = match &mut old_p.content {
1234                    FileContent::Dir(e) => e,
1235                    _ => return Err(FsError::NotADirectory),
1236                };
1237                let inode_id = old_entries.remove(old_name).ok_or(FsError::NotFound)?;
1238                old_p.metadata.modified = timestamp;
1239                let new_entries = match &mut new_p.content {
1240                    FileContent::Dir(e) => e,
1241                    _ => return Err(FsError::NotADirectory),
1242                };
1243                new_entries.insert(alloc::string::String::from(new_name), inode_id);
1244                new_p.metadata.modified = timestamp;
1245            } else {
1246                let mut new_p = inode_write!(new_parent);
1247                let mut old_p = inode_write!(old_parent);
1248                let old_entries = match &mut old_p.content {
1249                    FileContent::Dir(e) => e,
1250                    _ => return Err(FsError::NotADirectory),
1251                };
1252                let inode_id = old_entries.remove(old_name).ok_or(FsError::NotFound)?;
1253                old_p.metadata.modified = timestamp;
1254                let new_entries = match &mut new_p.content {
1255                    FileContent::Dir(e) => e,
1256                    _ => return Err(FsError::NotADirectory),
1257                };
1258                new_entries.insert(alloc::string::String::from(new_name), inode_id);
1259                new_p.metadata.modified = timestamp;
1260            }
1261        }
1262
1263        debug!("rename: {} -> {}", old_path, new_path);
1264        Ok(())
1265    }
1266}
1267
1268impl Default for Fs<MonotonicCounter> {
1269    fn default() -> Self {
1270        Self::new()
1271    }
1272}
1273
1274impl Fs<MonotonicCounter> {
1275    pub fn new() -> Self {
1276        Self::with_time_provider(MonotonicCounter::new())
1277    }
1278}
1279
1280// =============================================================================
1281// Constructor helpers (cfg-gated for the four storage primitives)
1282// =============================================================================
1283
1284#[cfg(feature = "thread-safe")]
1285fn new_fd_table() -> FdTable {
1286    DashMap::new()
1287}
1288#[cfg(all(feature = "std", not(feature = "thread-safe")))]
1289fn new_fd_table() -> FdTable {
1290    RefCell::new(HashMap::new())
1291}
1292#[cfg(not(feature = "std"))]
1293fn new_fd_table() -> FdTable {
1294    RefCell::new(BTreeMap::new())
1295}
1296
1297#[cfg(feature = "thread-safe")]
1298fn new_inode_table() -> InodeTable {
1299    DashMap::new()
1300}
1301#[cfg(all(feature = "std", not(feature = "thread-safe")))]
1302fn new_inode_table() -> InodeTable {
1303    RefCell::new(HashMap::new())
1304}
1305#[cfg(not(feature = "std"))]
1306fn new_inode_table() -> InodeTable {
1307    RefCell::new(BTreeMap::new())
1308}
1309
1310#[cfg(feature = "thread-safe")]
1311fn new_inode_counter(initial: InodeId) -> NextInodeCounter {
1312    AtomicU64::new(initial)
1313}
1314#[cfg(not(feature = "thread-safe"))]
1315fn new_inode_counter(initial: InodeId) -> NextInodeCounter {
1316    Cell::new(initial)
1317}
1318
1319#[cfg(feature = "thread-safe")]
1320fn new_fd_counter(initial: Fd) -> NextFdCounter {
1321    AtomicU32::new(initial)
1322}
1323#[cfg(not(feature = "thread-safe"))]
1324fn new_fd_counter(initial: Fd) -> NextFdCounter {
1325    Cell::new(initial)
1326}