nfs3_server/memfs/
mod.rs

1//! In-memory file system for `NFSv3`.
2//!
3//! It is a simple implementation of a file system that stores files and directories in memory.
4//! This file system is used for testing purposes and is not intended for production use.
5//!
6//! # Limitations
7//!
8//! - It's a very naive implementation and does not guarantee the best performance.
9//! - Methods `symlink` and `readlink` are not implemented and return `NFS3ERR_NOTSUPP`.
10//!
11//! # Examples
12//!
13//! ```no_run
14//! use nfs3_server::memfs::{MemFs, MemFsConfig};
15//! use nfs3_server::tcp::NFSTcpListener;
16//!
17//! async fn run() -> anyhow::Result<()> {
18//!     let mut config = MemFsConfig::default();
19//!     config.add_file("/a.txt", "hello world\n".as_bytes());
20//!     config.add_file("/b.txt", "Greetings\n".as_bytes());
21//!     config.add_dir("/a directory");
22//!
23//!     let memfs = MemFs::new(config).unwrap();
24//!     let listener = NFSTcpListener::bind("0.0.0.0:11111", memfs).await?;
25//!     listener.handle_forever().await?;
26//!     Ok(())
27//! }
28//! ```
29
30mod config;
31
32use std::collections::{HashMap, HashSet};
33use std::sync::atomic::AtomicU64;
34use std::sync::{Arc, RwLock};
35use std::time::SystemTime;
36
37pub use config::MemFsConfig;
38use nfs3_types::nfs3::{
39    self as nfs, cookie3, createverf3, entryplus3, fattr3, filename3, ftype3, nfspath3, nfsstat3,
40    nfstime3, sattr3, specdata3,
41};
42use nfs3_types::xdr_codec::Opaque;
43
44use crate::vfs::{
45    FileHandleU64, NextResult, NfsFileSystem, NfsReadFileSystem, ReadDirIterator,
46    ReadDirPlusIterator,
47};
48
49const DELIMITER: char = '/';
50
51#[derive(Debug)]
52struct Dir {
53    name: filename3<'static>,
54    parent: FileHandleU64,
55    attr: fattr3,
56    content: HashSet<FileHandleU64>,
57}
58
59impl Dir {
60    fn new(name: filename3<'static>, id: FileHandleU64, parent: FileHandleU64) -> Self {
61        let current_time = current_time();
62        let attr = fattr3 {
63            type_: ftype3::NF3DIR,
64            mode: 0o777,
65            nlink: 1,
66            uid: 507,
67            gid: 507,
68            size: 0,
69            used: 0,
70            rdev: specdata3::default(),
71            fsid: 0,
72            fileid: id.into(),
73            atime: current_time.clone(),
74            mtime: current_time.clone(),
75            ctime: current_time,
76        };
77        Self {
78            name,
79            parent,
80            attr,
81            content: HashSet::new(),
82        }
83    }
84
85    fn root_dir() -> Self {
86        let name = filename3(Opaque::borrowed(b"/"));
87        let id = 1.into();
88        Self::new(name, id, 0.into())
89    }
90
91    fn add_entry(&mut self, entry: FileHandleU64) -> bool {
92        self.content.insert(entry)
93    }
94}
95
96#[derive(Debug)]
97struct File {
98    name: filename3<'static>,
99    attr: fattr3,
100    content: Vec<u8>,
101    verf: createverf3,
102}
103
104impl File {
105    fn new(
106        name: filename3<'static>,
107        id: FileHandleU64,
108        content: Vec<u8>,
109        verf: createverf3,
110    ) -> Self {
111        let current_time = current_time();
112        let attr = fattr3 {
113            type_: ftype3::NF3REG,
114            mode: 0o755,
115            nlink: 1,
116            uid: 507,
117            gid: 507,
118            size: content.len() as u64,
119            used: content.len() as u64,
120            rdev: specdata3::default(),
121            fsid: 0,
122            fileid: id.into(),
123            atime: current_time.clone(),
124            mtime: current_time.clone(),
125            ctime: current_time,
126        };
127        Self {
128            name,
129            attr,
130            content,
131            verf,
132        }
133    }
134
135    fn fileid(&self) -> FileHandleU64 {
136        self.attr.fileid.into()
137    }
138
139    fn resize(&mut self, size: u64) {
140        self.content
141            .resize(usize::try_from(size).expect("size is too large"), 0);
142        self.attr.size = size;
143        self.attr.used = size;
144    }
145
146    fn read(&self, offset: u64, count: u32) -> (Vec<u8>, bool) {
147        let mut start = usize::try_from(offset).unwrap_or(usize::MAX);
148        let mut end = start + count as usize;
149        let bytes = &self.content;
150        let eof = end >= bytes.len();
151        if start >= bytes.len() {
152            start = bytes.len();
153        }
154        if end > bytes.len() {
155            end = bytes.len();
156        }
157        (bytes[start..end].to_vec(), eof)
158    }
159
160    #[allow(clippy::cast_possible_truncation)]
161    fn write(&mut self, offset: u64, data: &[u8]) -> Result<fattr3, nfsstat3> {
162        if offset > self.content.len() as u64 {
163            return Err(nfsstat3::NFS3ERR_INVAL);
164        }
165
166        let offset = offset as usize;
167        let end_offset = offset + data.len();
168        if end_offset > self.content.len() {
169            self.resize(end_offset as u64);
170        }
171        self.content[offset..end_offset].copy_from_slice(data);
172        Ok(self.attr.clone())
173    }
174}
175
176fn current_time() -> nfstime3 {
177    let d = SystemTime::now()
178        .duration_since(SystemTime::UNIX_EPOCH)
179        .expect("failed to get current time");
180    nfstime3 {
181        seconds: u32::try_from(d.as_secs()).unwrap_or(u32::MAX),
182        nseconds: d.subsec_nanos(),
183    }
184}
185
186#[derive(Debug)]
187enum Entry {
188    File(File),
189    Dir(Dir),
190}
191
192impl Entry {
193    fn new_file(
194        name: filename3<'static>,
195        parent: FileHandleU64,
196        content: Vec<u8>,
197        verf: createverf3,
198    ) -> Self {
199        Self::File(File::new(name, parent, content, verf))
200    }
201
202    fn new_dir(name: filename3<'static>, id: FileHandleU64, parent: FileHandleU64) -> Self {
203        Self::Dir(Dir::new(name, id, parent))
204    }
205
206    const fn as_dir(&self) -> Result<&Dir, nfsstat3> {
207        match self {
208            Self::Dir(dir) => Ok(dir),
209            Self::File(_) => Err(nfsstat3::NFS3ERR_NOTDIR),
210        }
211    }
212
213    const fn as_dir_mut(&mut self) -> Result<&mut Dir, nfsstat3> {
214        match self {
215            Self::Dir(dir) => Ok(dir),
216            Self::File(_) => Err(nfsstat3::NFS3ERR_NOTDIR),
217        }
218    }
219
220    const fn as_file(&self) -> Result<&File, nfsstat3> {
221        match self {
222            Self::File(file) => Ok(file),
223            Self::Dir(_) => Err(nfsstat3::NFS3ERR_ISDIR),
224        }
225    }
226
227    const fn as_file_mut(&mut self) -> Result<&mut File, nfsstat3> {
228        match self {
229            Self::File(file) => Ok(file),
230            Self::Dir(_) => Err(nfsstat3::NFS3ERR_ISDIR),
231        }
232    }
233
234    fn fileid(&self) -> FileHandleU64 {
235        match self {
236            Self::File(file) => file.attr.fileid,
237            Self::Dir(dir) => dir.attr.fileid,
238        }
239        .into()
240    }
241
242    const fn name(&self) -> &filename3<'static> {
243        match self {
244            Self::File(file) => &file.name,
245            Self::Dir(dir) => &dir.name,
246        }
247    }
248
249    fn set_name(&mut self, name: filename3<'static>) {
250        match self {
251            Self::File(file) => file.name = name,
252            Self::Dir(dir) => dir.name = name,
253        }
254    }
255
256    const fn attr(&self) -> &fattr3 {
257        match self {
258            Self::File(file) => &file.attr,
259            Self::Dir(dir) => &dir.attr,
260        }
261    }
262
263    const fn attr_mut(&mut self) -> &mut fattr3 {
264        match self {
265            Self::File(file) => &mut file.attr,
266            Self::Dir(dir) => &mut dir.attr,
267        }
268    }
269
270    fn set_attr(&mut self, setattr: sattr3) {
271        {
272            let attr = self.attr_mut();
273            match setattr.atime {
274                nfs::set_atime::DONT_CHANGE => {}
275                nfs::set_atime::SET_TO_CLIENT_TIME(c) => {
276                    attr.atime = c;
277                }
278                nfs::set_atime::SET_TO_SERVER_TIME => {
279                    attr.atime = current_time();
280                }
281            }
282            match setattr.mtime {
283                nfs::set_mtime::DONT_CHANGE => {}
284                nfs::set_mtime::SET_TO_CLIENT_TIME(c) => {
285                    attr.mtime = c;
286                }
287                nfs::set_mtime::SET_TO_SERVER_TIME => {
288                    attr.mtime = current_time();
289                }
290            }
291            if let nfs::set_uid3::Some(u) = setattr.uid {
292                attr.uid = u;
293            }
294            if let nfs::set_gid3::Some(u) = setattr.gid {
295                attr.gid = u;
296            }
297        }
298        if let nfs::set_size3::Some(s) = setattr.size {
299            if let Self::File(file) = self {
300                file.resize(s);
301            }
302        }
303    }
304}
305
306#[derive(Debug)]
307struct Fs {
308    entries: HashMap<FileHandleU64, Entry>,
309    root: FileHandleU64,
310}
311
312impl Fs {
313    fn new() -> Self {
314        let root = Entry::Dir(Dir::root_dir());
315        let fileid = root.fileid();
316        let mut flat_list = HashMap::new();
317        flat_list.insert(fileid, root);
318        Self {
319            entries: flat_list,
320            root: fileid,
321        }
322    }
323
324    fn push(&mut self, parent: FileHandleU64, entry: Entry) -> Result<(), nfsstat3> {
325        use std::collections::hash_map::Entry as MapEntry;
326
327        let id = entry.fileid();
328
329        let map_entry = self.entries.entry(id);
330        match map_entry {
331            MapEntry::Occupied(_) => {
332                tracing::warn!("object with same id already exists: {id}");
333                return Err(nfsstat3::NFS3ERR_EXIST);
334            }
335            MapEntry::Vacant(v) => {
336                v.insert(entry);
337            }
338        }
339
340        let parent_entry = self.entries.get_mut(&parent);
341        match parent_entry {
342            None => {
343                tracing::warn!("parent not found: {parent}");
344                self.entries.remove(&id); // remove the entry we just added
345                Err(nfsstat3::NFS3ERR_NOENT)
346            }
347            Some(Entry::File(_)) => {
348                tracing::warn!("parent is not a directory: {parent}");
349                self.entries.remove(&id); // remove the entry we just added
350                Err(nfsstat3::NFS3ERR_NOTDIR)
351            }
352            Some(Entry::Dir(dir)) => {
353                let added = dir.add_entry(id);
354                assert!(added, "failed to add a new entry to directory");
355                Ok(())
356            }
357        }
358    }
359
360    fn remove(&mut self, dirid: FileHandleU64, filename: &filename3) -> Result<(), nfsstat3> {
361        if filename.as_ref() == b"." || filename.as_ref() == b".." {
362            return Err(nfsstat3::NFS3ERR_INVAL);
363        }
364
365        let object_id = {
366            let entry = self.entries.get(&dirid).ok_or(nfsstat3::NFS3ERR_NOENT)?;
367            let dir = entry.as_dir()?;
368            let id = dir
369                .content
370                .iter()
371                .find(|i| self.entries.get(i).is_some_and(|f| f.name() == filename));
372            id.copied().ok_or(nfsstat3::NFS3ERR_NOENT)?
373        };
374
375        let entry = self
376            .entries
377            .get(&object_id)
378            .ok_or(nfsstat3::NFS3ERR_NOENT)?;
379        if let Entry::Dir(dir) = entry {
380            if !dir.content.is_empty() {
381                return Err(nfsstat3::NFS3ERR_NOTEMPTY);
382            }
383        }
384
385        self.entries.remove(&object_id);
386        self.entries
387            .get_mut(&dirid)
388            .expect("entry not found")
389            .as_dir_mut()?
390            .content
391            .remove(&object_id);
392        Ok(())
393    }
394
395    fn get(&self, id: FileHandleU64) -> Option<&Entry> {
396        self.entries.get(&id)
397    }
398
399    fn get_mut(&mut self, id: FileHandleU64) -> Option<&mut Entry> {
400        self.entries.get_mut(&id)
401    }
402
403    fn lookup(&self, dirid: FileHandleU64, filename: &filename3) -> Result<&Entry, nfsstat3> {
404        let entry = self.get(dirid).ok_or(nfsstat3::NFS3ERR_NOENT)?;
405
406        if let Entry::File(_) = entry {
407            return Err(nfsstat3::NFS3ERR_NOTDIR);
408        } else if let Entry::Dir(dir) = &entry {
409            // if looking for dir/. its the current directory
410            if filename.as_ref() == b"." {
411                return Ok(entry);
412            }
413            // if looking for dir/.. its the parent directory
414            if filename.as_ref() == b".." {
415                let parent = self.get(dir.parent).ok_or(nfsstat3::NFS3ERR_SERVERFAULT)?;
416                return Ok(parent);
417            }
418            for i in dir.content.iter().copied() {
419                match self.get(i) {
420                    None => {
421                        tracing::error!("invalid entry: {i}");
422                        return Err(nfsstat3::NFS3ERR_SERVERFAULT);
423                    }
424                    Some(f) => {
425                        if f.name() == filename {
426                            return Ok(f);
427                        }
428                    }
429                }
430            }
431        }
432        Err(nfsstat3::NFS3ERR_NOENT)
433    }
434
435    fn rename(
436        &mut self,
437        from_dirid: FileHandleU64,
438        from_filename: &filename3<'_>,
439        to_dirid: FileHandleU64,
440        to_filename: &filename3<'_>,
441    ) -> Result<(), nfsstat3> {
442        let from_entry = self.lookup(from_dirid, from_filename)?;
443        let from_id = from_entry.fileid();
444        let is_dir = matches!(from_entry, Entry::Dir(_));
445
446        // ✔️ source entry exists
447
448        if from_dirid == to_dirid && from_filename == to_filename {
449            // if source and target are the same, we can skip the rename
450            return Ok(());
451        }
452
453        let to_entry = self.lookup(to_dirid, to_filename);
454
455        // ✔️ target directory exists
456
457        match (from_entry, to_entry) {
458            (_, Err(nfsstat3::NFS3ERR_NOENT)) => {
459                // the entry does not exist, we can rename
460            }
461            (Entry::File(_), Ok(Entry::File(_))) => {
462                // if both entries are files, we can rename
463                self.remove(to_dirid, to_filename)?;
464            }
465            (Entry::Dir(_), Ok(Entry::Dir(tgt_dir))) => {
466                // if both entries are directories, we can rename if the target directory is empty
467                if !tgt_dir.content.is_empty() {
468                    tracing::warn!("target directory is not empty");
469                    return Err(nfsstat3::NFS3ERR_NOTEMPTY);
470                }
471                self.remove(to_dirid, to_filename)?;
472            }
473            (Entry::File(_), Ok(Entry::Dir(_))) => {
474                // cannot rename a file to a directory
475                tracing::warn!("cannot rename file to directory");
476                return Err(nfsstat3::NFS3ERR_NOTDIR);
477            }
478            (Entry::Dir(_), Ok(Entry::File(_))) => {
479                // cannot rename a directory to a file
480                tracing::warn!("cannot rename directory to file");
481                return Err(nfsstat3::NFS3ERR_NOTDIR);
482            }
483            (_, Err(e)) => {
484                // unexpected error, we should not continue
485                return Err(e);
486            }
487        }
488
489        // ✔️ target entry doesn't exist
490
491        // Prevent renaming a directory into its own subdirectory
492        if is_dir {
493            let mut current = to_dirid;
494            loop {
495                if current == from_id {
496                    tracing::warn!("cannot move a directory into its own subdirectory");
497                    return Err(nfsstat3::NFS3ERR_INVAL);
498                }
499                let entry = self.get(current).ok_or(nfsstat3::NFS3ERR_NOENT)?;
500                match entry {
501                    Entry::Dir(dir) => {
502                        if current == self.root {
503                            // Reached root
504                            break;
505                        }
506                        current = dir.parent;
507                    }
508                    Entry::File(_) => {
509                        tracing::error!("expected a directory, found a file");
510                        return Err(nfsstat3::NFS3ERR_SERVERFAULT);
511                    }
512                }
513            }
514        }
515
516        // Remove from old parent directory
517        {
518            let from_dir = self
519                .get_mut(from_dirid)
520                .ok_or(nfsstat3::NFS3ERR_SERVERFAULT)?;
521            from_dir.as_dir_mut()?.content.remove(&from_id);
522        }
523
524        // Add to new parent directory
525        {
526            let to_dir = self
527                .get_mut(to_dirid)
528                .ok_or(nfsstat3::NFS3ERR_SERVERFAULT)?;
529            let added = to_dir.as_dir_mut()?.content.insert(from_id);
530            if !added {
531                tracing::error!("failed to add entry to target directory");
532                return Err(nfsstat3::NFS3ERR_SERVERFAULT);
533            }
534        }
535
536        // Update entry's name and parent if needed
537        let entry = self.get_mut(from_id).ok_or(nfsstat3::NFS3ERR_SERVERFAULT)?;
538        entry.set_name(to_filename.clone_to_owned());
539        if let Entry::Dir(dir) = entry {
540            dir.parent = to_dirid;
541        }
542
543        Ok(())
544    }
545}
546
547/// In-memory file system for `NFSv3`.
548///
549/// `MemFs` implements the [`NfsFileSystem`] trait and provides a simple in-memory file system
550#[derive(Debug)]
551pub struct MemFs {
552    fs: Arc<RwLock<Fs>>,
553    rootdir: FileHandleU64,
554    nextid: AtomicU64,
555}
556
557impl Default for MemFs {
558    fn default() -> Self {
559        let root = Fs::new();
560        let rootdir = root.root;
561        let nextid = AtomicU64::new(rootdir.as_u64() + 1);
562        Self {
563            fs: Arc::new(RwLock::new(root)),
564            rootdir,
565            nextid,
566        }
567    }
568}
569
570impl MemFs {
571    /// Creates a new in-memory file system with the given configuration.
572    pub fn new(config: MemFsConfig) -> Result<Self, nfsstat3> {
573        tracing::info!("creating memfs. Entries count: {}", config.entries.len());
574        let fs = Self::default();
575
576        for entry in config.entries {
577            let id = fs.path_to_id_impl(&entry.parent)?;
578            let name = filename3(Opaque::owned(entry.name.into_bytes()));
579            if entry.is_dir {
580                fs.add_dir(id, name)?;
581            } else {
582                fs.add_file(id, name, sattr3::default(), entry.content, None)?;
583            }
584        }
585
586        Ok(fs)
587    }
588
589    fn add_dir(
590        &self,
591        dirid: FileHandleU64,
592        dirname: filename3<'static>,
593    ) -> Result<(FileHandleU64, fattr3), nfsstat3> {
594        let newid: FileHandleU64 = self
595            .nextid
596            .fetch_add(1, std::sync::atomic::Ordering::Relaxed)
597            .into();
598
599        let dir = Entry::new_dir(dirname, newid, dirid);
600        let attr = dir.attr().clone();
601
602        self.fs
603            .write()
604            .expect("lock is poisoned")
605            .push(dirid, dir)?;
606
607        Ok((newid, attr))
608    }
609
610    fn add_file(
611        &self,
612        dirid: FileHandleU64,
613        filename: filename3<'static>,
614        attr: sattr3,
615        content: Vec<u8>,
616        verf: Option<createverf3>,
617    ) -> Result<(FileHandleU64, fattr3), nfsstat3> {
618        let newid: FileHandleU64 = self
619            .nextid
620            .fetch_add(1, std::sync::atomic::Ordering::Relaxed)
621            .into();
622
623        let mut file = Entry::new_file(filename, newid, content, verf.unwrap_or_default());
624        file.set_attr(attr);
625        let attr = file.attr().clone();
626
627        let mut fs_lock = self.fs.write().expect("lock is poisoned");
628        match fs_lock.lookup(dirid, file.name()) {
629            Err(nfsstat3::NFS3ERR_NOENT) => {
630                // the existing file does not exist, we can add the new file
631            }
632            Ok(existing_file) => {
633                if let Entry::File(existing_file) = existing_file {
634                    if verf.is_some_and(|v| v == existing_file.verf) {
635                        return Ok((existing_file.fileid(), attr));
636                    }
637                }
638                return Err(nfsstat3::NFS3ERR_EXIST);
639            }
640            Err(e) => {
641                // unexpected error, we should not continue
642                return Err(e);
643            }
644        }
645        fs_lock.push(dirid, file)?;
646
647        Ok((newid, attr))
648    }
649
650    fn path_to_id_impl(&self, path: &str) -> Result<FileHandleU64, nfsstat3> {
651        let splits = path.split(DELIMITER);
652        let mut fid = self.root_dir();
653        let fs = self.fs.read().expect("lock is poisoned");
654        for component in splits {
655            if component.is_empty() {
656                continue;
657            }
658            let entry = fs.lookup(fid, &component.as_bytes().into())?;
659            fid = entry.fileid();
660        }
661        Ok(fid)
662    }
663
664    fn make_iter(
665        &self,
666        dirid: FileHandleU64,
667        start_after: cookie3,
668    ) -> Result<MemFsIterator, nfsstat3> {
669        let fs = self.fs.read().expect("lock is poisoned");
670        let entry = fs.get(dirid).ok_or(nfsstat3::NFS3ERR_NOENT)?;
671        let dir = entry.as_dir()?;
672
673        let mut iter = dir.content.iter();
674        if start_after != 0 {
675            // skip to the start_after entry
676            let find_result = iter.find(|i| **i == start_after);
677            if find_result.is_none() {
678                return Err(nfsstat3::NFS3ERR_BAD_COOKIE);
679            }
680        }
681        let content: Vec<_> = iter.copied().collect();
682        Ok(MemFsIterator::new(self.fs.clone(), content))
683    }
684}
685
686impl NfsReadFileSystem for MemFs {
687    type Handle = FileHandleU64;
688
689    fn root_dir(&self) -> FileHandleU64 {
690        self.rootdir
691    }
692
693    async fn lookup(
694        &self,
695        dirid: &FileHandleU64,
696        filename: &filename3<'_>,
697    ) -> Result<FileHandleU64, nfsstat3> {
698        let fs = self.fs.read().expect("lock is poisoned");
699        fs.lookup(*dirid, filename).map(Entry::fileid)
700    }
701
702    async fn getattr(&self, id: &FileHandleU64) -> Result<fattr3, nfsstat3> {
703        let fs = self.fs.read().expect("lock is poisoned");
704        let entry = fs.get(*id).ok_or(nfsstat3::NFS3ERR_NOENT)?;
705        Ok(entry.attr().clone())
706    }
707
708    async fn read(
709        &self,
710        id: &FileHandleU64,
711        offset: u64,
712        count: u32,
713    ) -> Result<(Vec<u8>, bool), nfsstat3> {
714        let fs = self.fs.read().expect("lock is poisoned");
715        let entry = fs.get(*id).ok_or(nfsstat3::NFS3ERR_NOENT)?;
716        let file = entry.as_file()?;
717        Ok(file.read(offset, count))
718    }
719
720    async fn readdir(
721        &self,
722        dirid: &FileHandleU64,
723        cookie: u64,
724    ) -> Result<impl ReadDirIterator, nfsstat3> {
725        let iter = Self::make_iter(self, *dirid, cookie)?;
726        Ok(iter)
727    }
728
729    async fn readdirplus(
730        &self,
731        dirid: &FileHandleU64,
732        cookie: u64,
733    ) -> Result<impl ReadDirPlusIterator, nfsstat3> {
734        let iter = Self::make_iter(self, *dirid, cookie)?;
735        Ok(iter)
736    }
737
738    async fn readlink(&self, _id: &FileHandleU64) -> Result<nfspath3, nfsstat3> {
739        tracing::warn!("readlink not implemented");
740        Err(nfsstat3::NFS3ERR_NOTSUPP)
741    }
742
743    async fn lookup_by_path(&self, path: &str) -> Result<FileHandleU64, nfsstat3> {
744        self.path_to_id_impl(path)
745    }
746}
747
748impl NfsFileSystem for MemFs {
749    async fn setattr(&self, id: &FileHandleU64, setattr: sattr3) -> Result<fattr3, nfsstat3> {
750        let mut fs = self.fs.write().expect("lock is poisoned");
751        let entry = fs.get_mut(*id).ok_or(nfsstat3::NFS3ERR_NOENT)?;
752        entry.set_attr(setattr);
753        Ok(entry.attr().clone())
754    }
755
756    async fn write(
757        &self,
758        id: &FileHandleU64,
759        offset: u64,
760        data: &[u8],
761    ) -> Result<fattr3, nfsstat3> {
762        let mut fs = self.fs.write().expect("lock is poisoned");
763
764        let entry = fs.get_mut(*id).ok_or(nfsstat3::NFS3ERR_NOENT)?;
765        let file = entry.as_file_mut().map_err(|_| nfsstat3::NFS3ERR_INVAL)?;
766        file.write(offset, data)
767    }
768
769    async fn create(
770        &self,
771        dirid: &FileHandleU64,
772        filename: &filename3<'_>,
773        attr: sattr3,
774    ) -> Result<(FileHandleU64, fattr3), nfsstat3> {
775        self.add_file(*dirid, filename.clone_to_owned(), attr, Vec::new(), None)
776    }
777
778    async fn create_exclusive(
779        &self,
780        dirid: &FileHandleU64,
781        filename: &filename3<'_>,
782        createverf: nfs::createverf3,
783    ) -> Result<FileHandleU64, nfsstat3> {
784        self.add_file(
785            *dirid,
786            filename.clone_to_owned(),
787            sattr3::default(),
788            Vec::new(),
789            Some(createverf),
790        )
791        .map(|(id, _attr)| id)
792    }
793
794    async fn mkdir(
795        &self,
796        dirid: &FileHandleU64,
797        dirname: &filename3<'_>,
798    ) -> Result<(FileHandleU64, fattr3), nfsstat3> {
799        self.add_dir(*dirid, dirname.clone_to_owned())
800    }
801
802    async fn remove(
803        &self,
804        dirid: &FileHandleU64,
805        filename: &filename3<'_>,
806    ) -> Result<(), nfsstat3> {
807        self.fs
808            .write()
809            .expect("lock is poisoned")
810            .remove(*dirid, filename)
811    }
812
813    async fn rename<'a>(
814        &self,
815        from_dirid: &FileHandleU64,
816        from_filename: &filename3<'a>,
817        to_dirid: &FileHandleU64,
818        to_filename: &filename3<'a>,
819    ) -> Result<(), nfsstat3> {
820        let mut fs = self.fs.write().expect("lock is poisoned");
821        fs.rename(*from_dirid, from_filename, *to_dirid, to_filename)
822    }
823
824    async fn symlink<'a>(
825        &self,
826        _dirid: &FileHandleU64,
827        _linkname: &filename3<'a>,
828        _symlink: &nfspath3<'a>,
829        _attr: &sattr3,
830    ) -> Result<(FileHandleU64, fattr3), nfsstat3> {
831        tracing::warn!("symlink not implemented");
832        Err(nfsstat3::NFS3ERR_NOTSUPP)
833    }
834}
835
836struct MemFsIterator {
837    fs: Arc<RwLock<Fs>>,
838    entries: Vec<FileHandleU64>,
839    index: usize,
840}
841
842impl MemFsIterator {
843    const fn new(fs: Arc<RwLock<Fs>>, entries: Vec<FileHandleU64>) -> Self {
844        Self {
845            fs,
846            entries,
847            index: 0,
848        }
849    }
850}
851
852impl ReadDirPlusIterator for MemFsIterator {
853    async fn next(&mut self) -> NextResult<entryplus3<'static>> {
854        loop {
855            if self.index >= self.entries.len() {
856                return NextResult::Eof;
857            }
858            let id = self.entries[self.index];
859            self.index += 1;
860
861            let fs = self.fs.read().expect("lock is poisoned");
862            let entry = fs.get(id);
863            let Some(entry) = entry else {
864                // skip missing entries
865                tracing::warn!("entry not found: {id}");
866                continue;
867            };
868            let attr = entry.attr().clone();
869            // let fh = DEFAULT_FH_CONVERTER.id_to_fh(id);
870            return NextResult::Ok(entryplus3 {
871                fileid: id.into(),
872                name: entry.name().clone_to_owned(),
873                cookie: id.into(),
874                name_attributes: nfs::post_op_attr::Some(attr),
875                name_handle: nfs::Nfs3Option::None,
876            });
877        }
878    }
879}