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);
}