fernfs 0.1.5

A Rust NFS Server implementation
Documentation
use std::io::Cursor;
use std::sync::{Arc, Mutex, RwLock};
use std::time::Duration;

use async_trait::async_trait;
use num_traits::FromPrimitive;

use fernfs::protocol::nfs::portmap::PortmapTable;
use fernfs::protocol::nfs::v3::handle_nfs;
use fernfs::protocol::rpc::{Context, TransactionTracker};
use fernfs::vfs::{self, Capabilities, NFSFileSystem, ReadDirResult};
use fernfs::xdr::{self, nfs3, Serialize};

const FILE_ID: nfs3::fileid3 = 1;

#[derive(Default)]
struct WriteCaptureFS {
    captured: Mutex<Option<nfs3::file::stable_how>>,
}

#[async_trait]
impl vfs::NFSFileSystem for WriteCaptureFS {
    fn generation(&self) -> u64 {
        1
    }

    fn capabilities(&self) -> Capabilities {
        Capabilities::ReadWrite
    }

    fn root_dir(&self) -> nfs3::fileid3 {
        FILE_ID
    }

    async fn lookup(
        &self,
        _dirid: nfs3::fileid3,
        _filename: &nfs3::filename3,
    ) -> Result<nfs3::fileid3, nfs3::nfsstat3> {
        Err(nfs3::nfsstat3::NFS3ERR_NOTSUPP)
    }

    async fn getattr(&self, id: nfs3::fileid3) -> Result<nfs3::fattr3, nfs3::nfsstat3> {
        if id != FILE_ID {
            return Err(nfs3::nfsstat3::NFS3ERR_NOENT);
        }
        Ok(nfs3::fattr3 { ftype: nfs3::ftype3::NF3REG, fileid: id, ..Default::default() })
    }

    async fn setattr(
        &self,
        _id: nfs3::fileid3,
        _setattr: nfs3::sattr3,
    ) -> Result<nfs3::fattr3, nfs3::nfsstat3> {
        Err(nfs3::nfsstat3::NFS3ERR_NOTSUPP)
    }

    async fn read(
        &self,
        _id: nfs3::fileid3,
        _offset: u64,
        _count: u32,
    ) -> Result<(Vec<u8>, bool), nfs3::nfsstat3> {
        Err(nfs3::nfsstat3::NFS3ERR_NOTSUPP)
    }

    async fn write(
        &self,
        _id: nfs3::fileid3,
        _offset: u64,
        _data: &[u8],
        stable: nfs3::file::stable_how,
    ) -> Result<(nfs3::fattr3, nfs3::file::stable_how, nfs3::count3), nfs3::nfsstat3> {
        *self.captured.lock().unwrap() = Some(stable);
        Ok((
            nfs3::fattr3 { ftype: nfs3::ftype3::NF3REG, fileid: FILE_ID, ..Default::default() },
            nfs3::file::stable_how::DATA_SYNC,
            4,
        ))
    }

    async fn create(
        &self,
        _dirid: nfs3::fileid3,
        _filename: &nfs3::filename3,
        _attr: nfs3::sattr3,
    ) -> Result<(nfs3::fileid3, nfs3::fattr3), nfs3::nfsstat3> {
        Err(nfs3::nfsstat3::NFS3ERR_NOTSUPP)
    }

    async fn create_exclusive(
        &self,
        _dirid: nfs3::fileid3,
        _filename: &nfs3::filename3,
        _verifier: nfs3::createverf3,
    ) -> Result<nfs3::fileid3, nfs3::nfsstat3> {
        Err(nfs3::nfsstat3::NFS3ERR_NOTSUPP)
    }

    async fn mkdir(
        &self,
        _dirid: nfs3::fileid3,
        _dirname: &nfs3::filename3,
    ) -> Result<(nfs3::fileid3, nfs3::fattr3), nfs3::nfsstat3> {
        Err(nfs3::nfsstat3::NFS3ERR_NOTSUPP)
    }

    async fn remove(
        &self,
        _dirid: nfs3::fileid3,
        _filename: &nfs3::filename3,
    ) -> Result<(), nfs3::nfsstat3> {
        Err(nfs3::nfsstat3::NFS3ERR_NOTSUPP)
    }

    async fn rename(
        &self,
        _from_dirid: nfs3::fileid3,
        _from_filename: &nfs3::filename3,
        _to_dirid: nfs3::fileid3,
        _to_filename: &nfs3::filename3,
    ) -> Result<(), nfs3::nfsstat3> {
        Err(nfs3::nfsstat3::NFS3ERR_NOTSUPP)
    }

    async fn readdir(
        &self,
        _dirid: nfs3::fileid3,
        _start_after: nfs3::fileid3,
        _max_entries: usize,
    ) -> Result<ReadDirResult, nfs3::nfsstat3> {
        Err(nfs3::nfsstat3::NFS3ERR_NOTSUPP)
    }

    async fn symlink(
        &self,
        _dirid: nfs3::fileid3,
        _linkname: &nfs3::filename3,
        _symlink: &nfs3::nfspath3,
        _attr: &nfs3::sattr3,
    ) -> Result<(nfs3::fileid3, nfs3::fattr3), nfs3::nfsstat3> {
        Err(nfs3::nfsstat3::NFS3ERR_NOTSUPP)
    }

    async fn readlink(&self, _id: nfs3::fileid3) -> Result<nfs3::nfspath3, nfs3::nfsstat3> {
        Err(nfs3::nfsstat3::NFS3ERR_NOTSUPP)
    }

    async fn link(
        &self,
        _file_id: nfs3::fileid3,
        _link_dir_id: nfs3::fileid3,
        _link_name: &nfs3::filename3,
    ) -> Result<nfs3::fattr3, nfs3::nfsstat3> {
        Err(nfs3::nfsstat3::NFS3ERR_NOTSUPP)
    }

    async fn mknod(
        &self,
        _dir_id: nfs3::fileid3,
        _name: &nfs3::filename3,
        _ftype: nfs3::ftype3,
        _specdata: nfs3::specdata3,
        _attrs: &nfs3::sattr3,
    ) -> Result<(nfs3::fileid3, nfs3::fattr3), nfs3::nfsstat3> {
        Err(nfs3::nfsstat3::NFS3ERR_NOTSUPP)
    }

    async fn commit(
        &self,
        _file_id: nfs3::fileid3,
        _offset: u64,
        _count: u32,
    ) -> Result<nfs3::fattr3, nfs3::nfsstat3> {
        Err(nfs3::nfsstat3::NFS3ERR_NOTSUPP)
    }
}

#[tokio::test]
async fn write_passes_stable_and_returns_committed() {
    let fs = Arc::new(WriteCaptureFS::default());
    let context = Context {
        local_port: 0,
        client_addr: "127.0.0.1:1234".to_string(),
        auth: xdr::rpc::auth_unix::default(),
        vfs: fs.clone(),
        mount_signal: None,
        export_name: Arc::from("/".to_string()),
        transaction_tracker: Arc::new(TransactionTracker::new(Duration::from_secs(60))),
        portmap_table: Arc::new(RwLock::new(PortmapTable::default())),
    };

    let file_handle = fs.id_to_fh(FILE_ID);
    let args = nfs3::file::WRITE3args {
        file: file_handle,
        offset: 0,
        count: 4,
        stable: nfs3::file::stable_how::FILE_SYNC as u32,
        data: b"data".to_vec(),
    };

    let mut input = Cursor::new(Vec::new());
    args.serialize(&mut input).expect("serialize write args");
    input.set_position(0);

    let call = xdr::rpc::call_body {
        rpcvers: 2,
        prog: nfs3::PROGRAM,
        vers: nfs3::VERSION,
        proc: nfs3::NFSProgram::NFSPROC3_WRITE as u32,
        cred: xdr::rpc::opaque_auth::default(),
        verf: xdr::rpc::opaque_auth::default(),
    };

    let mut output = Cursor::new(Vec::new());
    handle_nfs(11, call, &mut input, &mut output, &context).await.expect("handle_nfs");

    assert_eq!(*fs.captured.lock().unwrap(), Some(nfs3::file::stable_how::FILE_SYNC));

    output.set_position(0);
    let _rpc = xdr::deserialize::<xdr::rpc::rpc_msg>(&mut output).expect("deserialize rpc");
    let status_raw = xdr::deserialize::<u32>(&mut output).expect("deserialize status");
    let status = nfs3::nfsstat3::from_u32(status_raw).expect("invalid nfsstat3 value");
    assert_eq!(status, nfs3::nfsstat3::NFS3_OK);
    let res = xdr::deserialize::<nfs3::file::WRITE3resok>(&mut output).expect("deserialize resok");
    assert_eq!(res.committed, nfs3::file::stable_how::DATA_SYNC);
}