use bytes::{Buf, Bytes};
use super::{NFS4_COMPOUND_PROC, NFS4_PROGRAM, NFS4_VERSION, NFS41_MINOR_VERSION};
use crate::error::{NfsError, Result};
use crate::nfs3::rpc_header;
use crate::nfs4::compound::{
OP_GETFH, OP_LOOKUP, OP_PUTROOTFH, xdr_opaque as xdr_var_bytes, xdr_u32,
};
use crate::nfs4::fastxdr::*;
use crate::rpc::auth::Auth;
const MAX_COMPOUND_OPS: usize = 256;
const MAX_BITMAP_WORDS: usize = 16;
const MAX_ENTRY4_PER_PAGE: usize = 65536;
const MAX_LAYOUT_SEGMENTS: usize = 1024;
#[repr(u32)]
#[derive(Debug, Clone, Copy)]
pub(crate) enum OpNum {
Access = 3,
Close = 4,
Commit = 5,
Create = 6,
GetAttr = 9,
GetFh = OP_GETFH,
Link = 11,
Lookup = OP_LOOKUP,
Lookupp = 16,
Open = 18,
PutFh = 22,
PutRootFh = OP_PUTROOTFH,
Read = 25,
ReadDir = 26,
ReadLink = 27,
Remove = 28,
Rename = 29,
RestoreFh = 31,
SaveFh = 32,
SetAttr = 34,
Write = 38,
OpenAttr = 20,
Lock = 12,
Lockt = 13,
Locku = 14,
LayoutGet = 50,
LayoutCommit = 49,
LayoutReturn = 51,
GetDeviceInfo = 47,
ExchangeId = 42,
CreateSession = 43,
DestroySession = 44,
BindConnToSession = 41,
Sequence = 53,
ReclaimComplete = 58,
DestroyClientId = 57,
DelegReturn = 8,
TestStateId = 55,
FreeStateId = 56,
}
#[derive(Debug, Clone, Copy, Eq, Ord, PartialEq, PartialOrd)]
pub(crate) enum OperationClass {
ReadOnly,
SessionControl,
ReplaySensitive,
}
impl OpNum {
pub(crate) const fn class(self) -> OperationClass {
match self {
Self::Access
| Self::GetAttr
| Self::GetFh
| Self::GetDeviceInfo
| Self::Lockt
| Self::Lookup
| Self::Lookupp
| Self::PutFh
| Self::PutRootFh
| Self::Read
| Self::ReadDir
| Self::ReadLink
| Self::RestoreFh
| Self::SaveFh
| Self::TestStateId => OperationClass::ReadOnly,
Self::BindConnToSession
| Self::CreateSession
| Self::DestroyClientId
| Self::DestroySession
| Self::ExchangeId
| Self::ReclaimComplete
| Self::Sequence => OperationClass::SessionControl,
Self::Close
| Self::Commit
| Self::Create
| Self::DelegReturn
| Self::FreeStateId
| Self::LayoutCommit
| Self::LayoutGet
| Self::LayoutReturn
| Self::Link
| Self::Lock
| Self::Locku
| Self::Open
| Self::OpenAttr
| Self::Remove
| Self::Rename
| Self::SetAttr
| Self::Write => OperationClass::ReplaySensitive,
}
}
}
impl From<OperationClass> for crate::error::OperationClass {
fn from(value: OperationClass) -> Self {
match value {
OperationClass::ReadOnly => Self::ReadOnly,
OperationClass::SessionControl => Self::SessionControl,
OperationClass::ReplaySensitive => Self::ReplaySensitive,
}
}
}
fn xdr_u64(buf: &mut Vec<u8>, v: u64) {
buf.extend_from_slice(&v.to_be_bytes());
}
fn xdr_i64(buf: &mut Vec<u8>, v: i64) {
buf.extend_from_slice(&v.to_be_bytes());
}
fn xdr_bool(buf: &mut Vec<u8>, v: bool) {
xdr_u32(buf, if v { 1 } else { 0 });
}
fn xdr_string(buf: &mut Vec<u8>, s: &str) {
xdr_var_bytes(buf, s.as_bytes());
}
fn xdr_bitmap(buf: &mut Vec<u8>, bitmap: &[u32]) {
xdr_u32(buf, bitmap.len() as u32);
for &word in bitmap {
xdr_u32(buf, word);
}
}
pub(crate) struct CompoundBuilder {
tag: String,
ops: Vec<EncodedOp>,
required_generation: Option<u64>,
}
struct EncodedOp {
opcode: OpNum,
args: Vec<u8>,
}
const SEQUENCE_CACHE_THIS_OFFSET: usize = 28;
const MIN_RPC_REPLY_ENVELOPE_SIZE: usize = 24;
const MIN_COMPOUND_REPLY_ENVELOPE_SIZE: usize = 12;
const SEQUENCE_SUCCESS_REPLY_SIZE: usize = 44;
const MIN_OPERATION_REPLY_SIZE: usize = 8;
#[allow(dead_code)] impl CompoundBuilder {
pub fn new(tag: &str) -> Self {
Self {
tag: tag.to_string(),
ops: Vec::new(),
required_generation: None,
}
}
pub fn op_count(&self) -> usize {
self.ops.len()
}
pub(crate) fn enforce_max_operations(&self, maximum: u32) -> Result<()> {
let count = u32::try_from(self.ops.len())
.map_err(|_| NfsError::Rpc("COMPOUND operation count exceeds u32".to_string()))?;
if count == 0 || count > maximum {
return Err(NfsError::Rpc(format!(
"COMPOUND contains {count} operations; channel maximum is {maximum}"
)));
}
Ok(())
}
pub(crate) fn operation_class(&self) -> crate::error::OperationClass {
self.ops
.iter()
.skip(1)
.map(|op| op.opcode.class())
.max()
.unwrap_or(OperationClass::SessionControl)
.into()
}
pub(crate) fn require_generation(mut self, generation: u64) -> Self {
if generation != 0 {
self.required_generation = Some(generation);
}
self
}
pub(crate) fn required_generation(&self) -> Option<u64> {
self.required_generation
}
pub fn sequence(
mut self,
session_id: &[u8; 16],
sequence_id: u32,
slot_id: u32,
highest_slot_id: u32,
) -> Self {
let mut args = Vec::new();
args.extend_from_slice(session_id); xdr_u32(&mut args, sequence_id);
xdr_u32(&mut args, slot_id);
xdr_u32(&mut args, highest_slot_id);
xdr_bool(&mut args, false);
self.ops.push(EncodedOp {
opcode: OpNum::Sequence,
args,
});
self
}
pub fn apply_sequence_cache_policy(mut self, max_cached_response_size: u32) -> Result<Self> {
let class = self
.ops
.iter()
.skip(1)
.map(|op| op.opcode.class())
.max()
.unwrap_or(OperationClass::SessionControl);
let cachethis = class == OperationClass::ReplaySensitive;
let required_minimum = self.minimum_cached_response_size();
if cachethis && max_cached_response_size < required_minimum {
return Err(NfsError::Rpc(format!(
"replay-sensitive COMPOUND requires at least {} cached response bytes, server negotiated {}",
required_minimum, max_cached_response_size
)));
}
let sequence = self
.ops
.first_mut()
.ok_or_else(|| NfsError::Rpc("COMPOUND has no SEQUENCE operation".to_string()))?;
if sequence.opcode as u32 != OpNum::Sequence as u32
|| sequence.args.len() < SEQUENCE_CACHE_THIS_OFFSET + 4
{
return Err(NfsError::Rpc(
"COMPOUND first operation is not a valid SEQUENCE".to_string(),
));
}
sequence.args[SEQUENCE_CACHE_THIS_OFFSET..SEQUENCE_CACHE_THIS_OFFSET + 4]
.copy_from_slice(&(cachethis as u32).to_be_bytes());
Ok(self)
}
fn minimum_cached_response_size(&self) -> u32 {
let padded_tag_len = self.tag.len().saturating_add(3) & !3;
let remaining_ops = self.ops.len().saturating_sub(1);
let size = MIN_RPC_REPLY_ENVELOPE_SIZE
.saturating_add(MIN_COMPOUND_REPLY_ENVELOPE_SIZE)
.saturating_add(padded_tag_len)
.saturating_add(SEQUENCE_SUCCESS_REPLY_SIZE)
.saturating_add(remaining_ops.saturating_mul(MIN_OPERATION_REPLY_SIZE));
u32::try_from(size).unwrap_or(u32::MAX)
}
pub fn exchange_id(
mut self,
co_verifier: &[u8; 8],
co_ownerid: &[u8],
flags: u32,
impl_domain: &str,
impl_name: &str,
) -> Self {
let mut args = Vec::new();
args.extend_from_slice(co_verifier);
xdr_var_bytes(&mut args, co_ownerid);
xdr_u32(&mut args, flags);
xdr_u32(&mut args, 0);
xdr_u32(&mut args, 1);
xdr_string(&mut args, impl_domain);
xdr_string(&mut args, impl_name);
xdr_i64(&mut args, 0);
xdr_u32(&mut args, 0);
self.ops.push(EncodedOp {
opcode: OpNum::ExchangeId,
args,
});
self
}
pub fn create_session(
mut self,
client_id: u64,
sequence_id: u32,
flags: u32,
fore_attrs: &ChannelAttrsArgs,
back_attrs: &ChannelAttrsArgs,
cb_program: u32,
) -> Self {
let mut args = Vec::new();
xdr_u64(&mut args, client_id);
xdr_u32(&mut args, sequence_id);
xdr_u32(&mut args, flags);
fore_attrs.encode(&mut args);
back_attrs.encode(&mut args);
xdr_u32(&mut args, cb_program);
xdr_u32(&mut args, 1); xdr_u32(&mut args, 0); self.ops.push(EncodedOp {
opcode: OpNum::CreateSession,
args,
});
self
}
pub fn destroy_session(mut self, session_id: &[u8; 16]) -> Self {
let mut args = Vec::new();
args.extend_from_slice(session_id);
self.ops.push(EncodedOp {
opcode: OpNum::DestroySession,
args,
});
self
}
pub fn bind_conn_to_session(
mut self,
session_id: &[u8; 16],
dir: u32,
use_conn_in_rdma_mode: bool,
) -> Self {
let mut args = Vec::new();
args.extend_from_slice(session_id);
xdr_u32(&mut args, dir); xdr_bool(&mut args, use_conn_in_rdma_mode);
self.ops.push(EncodedOp {
opcode: OpNum::BindConnToSession,
args,
});
self
}
pub fn reclaim_complete(mut self, one_fs: bool) -> Self {
let mut args = Vec::new();
xdr_bool(&mut args, one_fs);
self.ops.push(EncodedOp {
opcode: OpNum::ReclaimComplete,
args,
});
self
}
pub fn destroy_client_id(mut self, client_id: u64) -> Self {
let mut args = Vec::new();
xdr_u64(&mut args, client_id);
self.ops.push(EncodedOp {
opcode: OpNum::DestroyClientId,
args,
});
self
}
pub fn putrootfh(mut self) -> Self {
self.ops.push(EncodedOp {
opcode: OpNum::PutRootFh,
args: Vec::new(),
});
self
}
pub fn putfh(mut self, fh: &[u8]) -> Self {
let mut args = Vec::new();
xdr_var_bytes(&mut args, fh);
self.ops.push(EncodedOp {
opcode: OpNum::PutFh,
args,
});
self
}
pub fn getfh(mut self) -> Self {
self.ops.push(EncodedOp {
opcode: OpNum::GetFh,
args: Vec::new(),
});
self
}
pub fn savefh(mut self) -> Self {
self.ops.push(EncodedOp {
opcode: OpNum::SaveFh,
args: Vec::new(),
});
self
}
pub fn restorefh(mut self) -> Self {
self.ops.push(EncodedOp {
opcode: OpNum::RestoreFh,
args: Vec::new(),
});
self
}
pub fn openattr(mut self, create_dir: bool) -> Self {
let mut args = Vec::new();
xdr_bool(&mut args, create_dir);
self.ops.push(EncodedOp {
opcode: OpNum::OpenAttr,
args,
});
self
}
pub fn lookup(mut self, name: &str) -> Self {
let mut args = Vec::new();
xdr_string(&mut args, name);
self.ops.push(EncodedOp {
opcode: OpNum::Lookup,
args,
});
self
}
pub fn lookupp(mut self) -> Self {
self.ops.push(EncodedOp {
opcode: OpNum::Lookupp,
args: Vec::new(),
});
self
}
pub fn getattr(mut self, bitmap: &[u32]) -> Self {
let mut args = Vec::new();
xdr_bitmap(&mut args, bitmap);
self.ops.push(EncodedOp {
opcode: OpNum::GetAttr,
args,
});
self
}
pub fn setattr(mut self, stateid: &[u8; 16], attrmask: &[u32], attr_vals: &[u8]) -> Self {
let mut args = Vec::new();
args.extend_from_slice(stateid);
xdr_bitmap(&mut args, attrmask);
xdr_var_bytes(&mut args, attr_vals);
self.ops.push(EncodedOp {
opcode: OpNum::SetAttr,
args,
});
self
}
pub fn access(mut self, access_mask: u32) -> Self {
let mut args = Vec::new();
xdr_u32(&mut args, access_mask);
self.ops.push(EncodedOp {
opcode: OpNum::Access,
args,
});
self
}
pub fn read(mut self, stateid: &[u8; 16], offset: u64, count: u32) -> Self {
let mut args = Vec::new();
args.extend_from_slice(stateid);
xdr_u64(&mut args, offset);
xdr_u32(&mut args, count);
self.ops.push(EncodedOp {
opcode: OpNum::Read,
args,
});
self
}
pub fn write(mut self, stateid: &[u8; 16], offset: u64, stable: u32, data: &[u8]) -> Self {
let mut args = Vec::new();
args.extend_from_slice(stateid);
xdr_u64(&mut args, offset);
xdr_u32(&mut args, stable); xdr_var_bytes(&mut args, data);
self.ops.push(EncodedOp {
opcode: OpNum::Write,
args,
});
self
}
pub fn write_header(
mut self,
stateid: &[u8; 16],
offset: u64,
stable: u32,
data_len: u32,
) -> Self {
let mut args = Vec::new();
args.extend_from_slice(stateid);
xdr_u64(&mut args, offset);
xdr_u32(&mut args, stable); xdr_u32(&mut args, data_len);
self.ops.push(EncodedOp {
opcode: OpNum::Write,
args,
});
self
}
pub fn commit(mut self, offset: u64, count: u32) -> Self {
let mut args = Vec::new();
xdr_u64(&mut args, offset);
xdr_u32(&mut args, count);
self.ops.push(EncodedOp {
opcode: OpNum::Commit,
args,
});
self
}
pub fn readdir(
mut self,
cookie: u64,
cookieverf: &[u8; 8],
dircount: u32,
maxcount: u32,
attr_request: &[u32],
) -> Self {
let mut args = Vec::new();
xdr_u64(&mut args, cookie);
args.extend_from_slice(cookieverf);
xdr_u32(&mut args, dircount);
xdr_u32(&mut args, maxcount);
xdr_bitmap(&mut args, attr_request);
self.ops.push(EncodedOp {
opcode: OpNum::ReadDir,
args,
});
self
}
pub fn readlink(mut self) -> Self {
self.ops.push(EncodedOp {
opcode: OpNum::ReadLink,
args: Vec::new(),
});
self
}
pub fn open(mut self, open_args: &OpenArgs) -> Self {
let mut args = Vec::new();
open_args.encode(&mut args);
self.ops.push(EncodedOp {
opcode: OpNum::Open,
args,
});
self
}
pub fn close(mut self, seqid: u32, stateid: &[u8; 16]) -> Self {
let mut args = Vec::new();
xdr_u32(&mut args, seqid);
args.extend_from_slice(stateid);
self.ops.push(EncodedOp {
opcode: OpNum::Close,
args,
});
self
}
pub fn create(mut self, objtype: u32, name: &str, attrmask: &[u32], attr_vals: &[u8]) -> Self {
let mut args = Vec::new();
xdr_u32(&mut args, objtype); xdr_string(&mut args, name);
xdr_bitmap(&mut args, attrmask);
xdr_var_bytes(&mut args, attr_vals);
self.ops.push(EncodedOp {
opcode: OpNum::Create,
args,
});
self
}
pub fn create_symlink(
mut self,
name: &str,
link_target: &str,
attrmask: &[u32],
attr_vals: &[u8],
) -> Self {
let mut args = Vec::new();
xdr_u32(&mut args, 5); xdr_string(&mut args, link_target);
xdr_string(&mut args, name);
xdr_bitmap(&mut args, attrmask);
xdr_var_bytes(&mut args, attr_vals);
self.ops.push(EncodedOp {
opcode: OpNum::Create,
args,
});
self
}
pub fn remove(mut self, name: &str) -> Self {
let mut args = Vec::new();
xdr_string(&mut args, name);
self.ops.push(EncodedOp {
opcode: OpNum::Remove,
args,
});
self
}
pub fn rename(mut self, oldname: &str, newname: &str) -> Self {
let mut args = Vec::new();
xdr_string(&mut args, oldname);
xdr_string(&mut args, newname);
self.ops.push(EncodedOp {
opcode: OpNum::Rename,
args,
});
self
}
pub fn link(mut self, newname: &str) -> Self {
let mut args = Vec::new();
xdr_string(&mut args, newname);
self.ops.push(EncodedOp {
opcode: OpNum::Link,
args,
});
self
}
#[allow(clippy::too_many_arguments)]
pub fn layoutget(
mut self,
signal_layout_avail: bool,
layout_type: u32,
iomode: u32,
offset: u64,
length: u64,
min_length: u64,
stateid: &[u8; 16],
max_count: u32,
) -> Self {
let mut args = Vec::new();
xdr_bool(&mut args, signal_layout_avail);
xdr_u32(&mut args, layout_type);
xdr_u32(&mut args, iomode);
xdr_u64(&mut args, offset);
xdr_u64(&mut args, length);
xdr_u64(&mut args, min_length);
args.extend_from_slice(stateid);
xdr_u32(&mut args, max_count);
self.ops.push(EncodedOp {
opcode: OpNum::LayoutGet,
args,
});
self
}
pub fn layoutcommit(
mut self,
offset: u64,
length: u64,
reclaim: bool,
stateid: &[u8; 16],
last_write_offset: Option<u64>,
layout_type: u32,
) -> Self {
let mut args = Vec::new();
xdr_u64(&mut args, offset);
xdr_u64(&mut args, length);
xdr_bool(&mut args, reclaim);
args.extend_from_slice(stateid);
if let Some(off) = last_write_offset {
xdr_bool(&mut args, true);
xdr_u64(&mut args, off);
} else {
xdr_bool(&mut args, false);
}
xdr_u32(&mut args, 0);
xdr_u32(&mut args, layout_type);
xdr_u32(&mut args, 0);
self.ops.push(EncodedOp {
opcode: OpNum::LayoutCommit,
args,
});
self
}
#[allow(clippy::too_many_arguments)]
pub fn layoutreturn(
mut self,
reclaim: bool,
layout_type: u32,
iomode: u32,
return_type: u32,
offset: u64,
length: u64,
stateid: &[u8; 16],
) -> Self {
let mut args = Vec::new();
xdr_bool(&mut args, reclaim);
xdr_u32(&mut args, layout_type);
xdr_u32(&mut args, iomode);
xdr_u32(&mut args, return_type);
if return_type == 1 {
xdr_u64(&mut args, offset);
xdr_u64(&mut args, length);
args.extend_from_slice(stateid);
xdr_u32(&mut args, 0);
}
self.ops.push(EncodedOp {
opcode: OpNum::LayoutReturn,
args,
});
self
}
pub fn getdeviceinfo(mut self, device_id: &[u8; 16], layout_type: u32, max_count: u32) -> Self {
let mut args = Vec::new();
args.extend_from_slice(device_id); xdr_u32(&mut args, layout_type);
xdr_u32(&mut args, max_count);
xdr_u32(&mut args, 0);
self.ops.push(EncodedOp {
opcode: OpNum::GetDeviceInfo,
args,
});
self
}
#[allow(clippy::too_many_arguments)]
pub fn lock(
mut self,
lock_type: u32,
reclaim: bool,
offset: u64,
length: u64,
new_lock_owner: bool,
open_stateid: &[u8; 16],
lock_seqid: u32,
open_seqid: u32,
lock_owner: &[u8],
client_id: u64,
) -> Self {
let mut args = Vec::new();
xdr_u32(&mut args, lock_type);
xdr_bool(&mut args, reclaim);
xdr_u64(&mut args, offset);
xdr_u64(&mut args, length);
if new_lock_owner {
xdr_bool(&mut args, true); xdr_u32(&mut args, open_seqid);
args.extend_from_slice(open_stateid); xdr_u32(&mut args, lock_seqid);
xdr_u64(&mut args, client_id);
xdr_var_bytes(&mut args, lock_owner);
} else {
xdr_bool(&mut args, false); args.extend_from_slice(open_stateid); xdr_u32(&mut args, lock_seqid);
}
self.ops.push(EncodedOp {
opcode: OpNum::Lock,
args,
});
self
}
pub fn lockt(
mut self,
lock_type: u32,
offset: u64,
length: u64,
lock_owner: &[u8],
client_id: u64,
) -> Self {
let mut args = Vec::new();
xdr_u32(&mut args, lock_type);
xdr_u64(&mut args, offset);
xdr_u64(&mut args, length);
xdr_u64(&mut args, client_id);
xdr_var_bytes(&mut args, lock_owner);
self.ops.push(EncodedOp {
opcode: OpNum::Lockt,
args,
});
self
}
pub fn locku(
mut self,
lock_type: u32,
seqid: u32,
lock_stateid: &[u8; 16],
offset: u64,
length: u64,
) -> Self {
let mut args = Vec::new();
xdr_u32(&mut args, lock_type);
xdr_u32(&mut args, seqid);
args.extend_from_slice(lock_stateid);
xdr_u64(&mut args, offset);
xdr_u64(&mut args, length);
self.ops.push(EncodedOp {
opcode: OpNum::Locku,
args,
});
self
}
pub fn test_stateid(mut self, stateids: &[[u8; 16]]) -> Self {
let mut args = Vec::new();
xdr_u32(&mut args, stateids.len() as u32);
for sid in stateids {
args.extend_from_slice(sid);
}
self.ops.push(EncodedOp {
opcode: OpNum::TestStateId,
args,
});
self
}
pub fn delegreturn(mut self, stateid: &[u8; 16]) -> Self {
let mut args = Vec::new();
args.extend_from_slice(stateid); self.ops.push(EncodedOp {
opcode: OpNum::DelegReturn,
args,
});
self
}
pub fn free_stateid(mut self, stateid: &[u8; 16]) -> Self {
let mut args = Vec::new();
args.extend_from_slice(stateid);
self.ops.push(EncodedOp {
opcode: OpNum::FreeStateId,
args,
});
self
}
pub fn encode_with_header(self, auth: &Auth, buf: &mut Vec<u8>) {
rpc_header(NFS4_PROGRAM, NFS4_VERSION, NFS4_COMPOUND_PROC, auth).encode(buf);
self.encode_body(buf);
}
fn encode_body(self, buf: &mut Vec<u8>) {
xdr_string(buf, &self.tag);
xdr_u32(buf, NFS41_MINOR_VERSION);
xdr_u32(buf, self.ops.len() as u32);
for op in self.ops {
xdr_u32(buf, op.opcode as u32);
buf.extend_from_slice(&op.args);
}
}
}
pub(crate) struct ChannelAttrsArgs {
pub headerpadsize: u32,
pub maxrequestsize: u32,
pub maxresponsesize: u32,
pub maxresponsesize_cached: u32,
pub maxoperations: u32,
pub maxrequests: u32,
}
impl ChannelAttrsArgs {
fn encode(&self, buf: &mut Vec<u8>) {
xdr_u32(buf, self.headerpadsize);
xdr_u32(buf, self.maxrequestsize);
xdr_u32(buf, self.maxresponsesize);
xdr_u32(buf, self.maxresponsesize_cached);
xdr_u32(buf, self.maxoperations);
xdr_u32(buf, self.maxrequests);
xdr_u32(buf, 0);
}
}
const OPEN4_SHARE_ACCESS_WANT_NO_DELEG: u32 = 0x0400;
const OPEN4_SHARE_ACCESS_WANT_WRITE_DELEG: u32 = 0x0200;
pub(crate) struct OpenArgs {
pub seqid: u32,
pub share_access: u32,
pub share_deny: u32,
pub client_id: u64,
pub owner: Bytes,
pub create: bool,
pub create_attrs_mask: Vec<u32>,
pub create_attrs_vals: Vec<u8>,
pub claim_file: String,
pub want_no_delegation: bool,
}
impl OpenArgs {
fn encode(&self, buf: &mut Vec<u8>) {
xdr_u32(buf, self.seqid);
let share_access = if self.want_no_delegation {
self.share_access | OPEN4_SHARE_ACCESS_WANT_NO_DELEG
} else {
self.share_access | OPEN4_SHARE_ACCESS_WANT_WRITE_DELEG
};
xdr_u32(buf, share_access);
xdr_u32(buf, self.share_deny);
xdr_u64(buf, self.client_id);
xdr_var_bytes(buf, &self.owner);
if self.create {
xdr_u32(buf, 1); xdr_u32(buf, 0); xdr_bitmap(buf, &self.create_attrs_mask);
xdr_var_bytes(buf, &self.create_attrs_vals);
} else {
xdr_u32(buf, 0); }
xdr_u32(buf, 0);
xdr_string(buf, &self.claim_file);
}
}
pub(crate) struct CompoundResponse {
#[cfg_attr(not(test), allow(dead_code))]
pub tag: String,
pub status: nfsstat4,
pub results: Vec<OpResponse>,
pub session_generation: u64,
}
pub(crate) struct OpResponse {
pub opcode: u32,
pub status: nfsstat4,
pub data: Bytes,
}
impl CompoundResponse {
pub fn decode(mut buf: Bytes) -> Result<Self> {
if buf.remaining() < 4 {
return Err(NfsError::Xdr("COMPOUND response too short".to_string()));
}
let status_val = buf.get_u32();
let status = decode_nfsstat4(status_val)?;
let tag = decode_string(&mut buf)?;
if buf.remaining() < 4 {
return Err(NfsError::Xdr(
"COMPOUND response missing resarray length".to_string(),
));
}
let num_results = buf.get_u32() as usize;
if num_results > MAX_COMPOUND_OPS {
return Err(NfsError::Xdr(format!(
"COMPOUND response has {} ops, max {}",
num_results, MAX_COMPOUND_OPS
)));
}
let mut results = Vec::with_capacity(num_results);
for _ in 0..num_results {
if buf.remaining() < 8 {
return Err(NfsError::Xdr(
"COMPOUND response truncated at op header".to_string(),
));
}
let opcode = buf.get_u32();
let op_status_val = buf.get_u32();
let op_status = decode_nfsstat4(op_status_val)?;
results.push(OpResponse {
opcode,
status: op_status,
data: buf.clone(), });
skip_op_result(opcode, op_status_val, &mut buf)?;
}
Ok(CompoundResponse {
tag,
status,
results,
session_generation: 0,
})
}
pub fn check_status(&self) -> Result<()> {
if matches!(self.status, nfsstat4::NFS4_OK) {
Ok(())
} else {
Err(NfsError::Nfs4(self.status))
}
}
pub fn op_ok(&self, index: usize) -> Result<&OpResponse> {
let op = self.results.get(index).ok_or_else(|| {
NfsError::Xdr(format!("COMPOUND response missing op at index {}", index))
})?;
if !matches!(op.status, nfsstat4::NFS4_OK) {
return Err(NfsError::Nfs4(op.status));
}
Ok(op)
}
}
fn decode_nfsstat4(val: u32) -> Result<nfsstat4> {
let be = val.to_be_bytes();
nfsstat4::try_from(&mut Bytes::copy_from_slice(&be))
.map_err(|e| NfsError::Xdr(format!("invalid nfsstat4 {}: {}", val, e)))
}
fn decode_string(buf: &mut Bytes) -> Result<String> {
if buf.remaining() < 4 {
return Err(NfsError::Xdr("string length truncated".to_string()));
}
let len = buf.get_u32() as usize;
let padded = (len + 3) & !3;
if buf.remaining() < padded {
return Err(NfsError::Xdr("string data truncated".to_string()));
}
let s = String::from_utf8(buf.slice(..len).to_vec())
.map_err(|e| NfsError::Xdr(format!("invalid UTF-8 in string: {}", e)))?;
buf.advance(padded);
Ok(s)
}
fn skip_var_bytes(buf: &mut Bytes) -> Result<()> {
if buf.remaining() < 4 {
return Err(NfsError::Xdr("opaque length truncated".to_string()));
}
let len = buf.get_u32() as usize;
let padded = (len + 3) & !3;
if buf.remaining() < padded {
return Err(NfsError::Xdr("opaque data truncated".to_string()));
}
buf.advance(padded);
Ok(())
}
fn skip_bitmap(buf: &mut Bytes) -> Result<()> {
if buf.remaining() < 4 {
return Err(NfsError::Xdr("bitmap length truncated".to_string()));
}
let n = buf.get_u32() as usize;
if n > MAX_BITMAP_WORDS {
return Err(NfsError::Xdr(format!(
"bitmap has {} words, max {}",
n, MAX_BITMAP_WORDS
)));
}
let bytes_needed = n * 4;
if buf.remaining() < bytes_needed {
return Err(NfsError::Xdr("bitmap data truncated".to_string()));
}
buf.advance(bytes_needed);
Ok(())
}
fn skip_fattr4(buf: &mut Bytes) -> Result<()> {
skip_bitmap(buf)?;
skip_var_bytes(buf)
}
fn skip_change_info(buf: &mut Bytes) -> Result<()> {
if buf.remaining() < 20 {
return Err(NfsError::Xdr("change_info truncated".to_string()));
}
buf.advance(20);
Ok(())
}
fn skip_stateid4(buf: &mut Bytes) -> Result<()> {
if buf.remaining() < 16 {
return Err(NfsError::Xdr("stateid4 truncated".to_string()));
}
buf.advance(16);
Ok(())
}
fn skip_op_result(opcode: u32, status: u32, buf: &mut Bytes) -> Result<()> {
if status != 0 {
if opcode == OpNum::SetAttr as u32 {
skip_bitmap(buf)?;
}
if opcode == OpNum::Lock as u32
&& status == 10012
&& buf.remaining() >= 28
{
buf.advance(28); skip_var_bytes(buf)?; }
return Ok(());
}
match opcode {
op if op == OpNum::PutRootFh as u32 => {}
op if op == OpNum::PutFh as u32 => {}
op if op == OpNum::SaveFh as u32 => {}
op if op == OpNum::RestoreFh as u32 => {}
op if op == OpNum::Lookup as u32 => {}
op if op == OpNum::Lookupp as u32 => {}
op if op == OpNum::ReclaimComplete as u32 => {}
op if op == OpNum::DestroySession as u32 => {}
op if op == OpNum::DestroyClientId as u32 => {}
op if op == OpNum::DelegReturn as u32 => {}
op if op == OpNum::Sequence as u32 => {
if buf.remaining() < 36 {
return Err(NfsError::Xdr("SEQUENCE result truncated".to_string()));
}
buf.advance(36);
}
op if op == OpNum::GetFh as u32 => {
skip_var_bytes(buf)?;
}
op if op == OpNum::GetAttr as u32 => {
skip_fattr4(buf)?;
}
op if op == OpNum::SetAttr as u32 => {
skip_bitmap(buf)?;
}
op if op == OpNum::Access as u32 => {
if buf.remaining() < 8 {
return Err(NfsError::Xdr("ACCESS result truncated".to_string()));
}
buf.advance(8);
}
op if op == OpNum::Read as u32 => {
if buf.remaining() < 4 {
return Err(NfsError::Xdr("READ result truncated".to_string()));
}
buf.advance(4); skip_var_bytes(buf)?; }
op if op == OpNum::Write as u32 => {
if buf.remaining() < 16 {
return Err(NfsError::Xdr("WRITE result truncated".to_string()));
}
buf.advance(16);
}
op if op == OpNum::Commit as u32 => {
if buf.remaining() < 8 {
return Err(NfsError::Xdr("COMMIT result truncated".to_string()));
}
buf.advance(8);
}
op if op == OpNum::ReadDir as u32 => {
if buf.remaining() < 8 {
return Err(NfsError::Xdr("READDIR result truncated".to_string()));
}
buf.advance(8); skip_entry4_list(buf)?;
}
op if op == OpNum::ReadLink as u32 => {
skip_var_bytes(buf)?;
}
op if op == OpNum::OpenAttr as u32 => {}
op if op == OpNum::Open as u32 => {
skip_stateid4(buf)?;
skip_change_info(buf)?;
if buf.remaining() < 4 {
return Err(NfsError::Xdr("OPEN rflags truncated".to_string()));
}
buf.advance(4); skip_bitmap(buf)?; skip_open_delegation(buf)?;
}
op if op == OpNum::Close as u32 => {
skip_stateid4(buf)?;
}
op if op == OpNum::Lock as u32 => {
skip_stateid4(buf)?;
}
op if op == OpNum::Lockt as u32 => {}
op if op == OpNum::Locku as u32 => {
skip_stateid4(buf)?;
}
op if op == OpNum::TestStateId as u32 => {
if buf.remaining() < 4 {
return Err(NfsError::Xdr("TEST_STATEID result truncated".to_string()));
}
let n = buf.get_u32() as usize;
let bytes_needed = n
.checked_mul(4)
.ok_or_else(|| NfsError::Xdr("TEST_STATEID count overflow".to_string()))?;
if buf.remaining() < bytes_needed {
return Err(NfsError::Xdr("TEST_STATEID statuses truncated".to_string()));
}
buf.advance(bytes_needed);
}
op if op == OpNum::FreeStateId as u32 => {}
op if op == OpNum::Create as u32 => {
skip_change_info(buf)?;
skip_bitmap(buf)?;
}
op if op == OpNum::Remove as u32 => {
skip_change_info(buf)?;
}
op if op == OpNum::Rename as u32 => {
skip_change_info(buf)?;
skip_change_info(buf)?;
}
op if op == OpNum::Link as u32 => {
skip_change_info(buf)?;
}
op if op == OpNum::ExchangeId as u32 => {
skip_exchange_id_result(buf)?;
}
op if op == OpNum::CreateSession as u32 => {
skip_create_session_result(buf)?;
}
op if op == OpNum::LayoutGet as u32 => {
if buf.remaining() < 20 {
return Err(NfsError::Xdr("LAYOUTGET result truncated".to_string()));
}
buf.advance(4); skip_stateid4(buf)?; if buf.remaining() < 4 {
return Err(NfsError::Xdr(
"LAYOUTGET segments len truncated".to_string(),
));
}
let n = buf.get_u32() as usize;
if n > MAX_LAYOUT_SEGMENTS {
return Err(NfsError::Xdr(format!(
"LAYOUTGET has {} segments, max {}",
n, MAX_LAYOUT_SEGMENTS
)));
}
for _ in 0..n {
if buf.remaining() < 24 {
return Err(NfsError::Xdr("layout4 segment truncated".to_string()));
}
buf.advance(24);
skip_var_bytes(buf)?; }
}
op if op == OpNum::LayoutCommit as u32 => {
if buf.remaining() < 4 {
return Err(NfsError::Xdr("LAYOUTCOMMIT result truncated".to_string()));
}
let has_newsize = buf.get_u32();
if has_newsize != 0 && buf.remaining() >= 8 {
buf.advance(8);
}
}
op if op == OpNum::LayoutReturn as u32 => {
if buf.remaining() < 4 {
return Err(NfsError::Xdr("LAYOUTRETURN result truncated".to_string()));
}
let has_stateid = buf.get_u32();
if has_stateid != 0 {
skip_stateid4(buf)?;
}
}
op if op == OpNum::GetDeviceInfo as u32 => {
if buf.remaining() < 4 {
return Err(NfsError::Xdr(
"GETDEVICEINFO layout_type truncated".to_string(),
));
}
buf.advance(4);
skip_var_bytes(buf)?; skip_bitmap(buf)?; }
op if op == OpNum::BindConnToSession as u32 => {
if buf.remaining() < 24 {
return Err(NfsError::Xdr(
"BIND_CONN_TO_SESSION result truncated".to_string(),
));
}
buf.advance(24);
}
_ => {
return Err(NfsError::Xdr(format!(
"unknown op {} in COMPOUND response, cannot skip",
opcode
)));
}
}
Ok(())
}
fn skip_entry4_list(buf: &mut Bytes) -> Result<()> {
let mut count = 0usize;
loop {
if buf.remaining() < 4 {
return Err(NfsError::Xdr("entry4 list truncated".to_string()));
}
let has_entry = buf.get_u32();
if has_entry == 0 {
if buf.remaining() < 4 {
return Err(NfsError::Xdr("dirlist4 eof truncated".to_string()));
}
buf.advance(4); return Ok(());
}
count += 1;
if count > MAX_ENTRY4_PER_PAGE {
return Err(NfsError::Xdr(format!(
"entry4 list exceeds max {}",
MAX_ENTRY4_PER_PAGE
)));
}
if buf.remaining() < 8 {
return Err(NfsError::Xdr("entry4 cookie truncated".to_string()));
}
buf.advance(8); skip_var_bytes(buf)?; skip_fattr4(buf)?; }
}
fn skip_open_delegation(buf: &mut Bytes) -> Result<()> {
if buf.remaining() < 4 {
return Err(NfsError::Xdr("open_delegation type truncated".to_string()));
}
let deleg_type = buf.get_u32();
match deleg_type {
0 => {} 1 => {
skip_stateid4(buf)?;
if buf.remaining() < 4 {
return Err(NfsError::Xdr("delegation recall truncated".to_string()));
}
buf.advance(4); let _ace = crate::nfs4::acl::decode_nfsace4(buf)?;
}
2 => {
skip_stateid4(buf)?;
if buf.remaining() < 4 {
return Err(NfsError::Xdr("delegation recall truncated".to_string()));
}
buf.advance(4); skip_space_limit(buf)?;
let _ace = crate::nfs4::acl::decode_nfsace4(buf)?;
}
3 => {
if buf.remaining() < 4 {
return Err(NfsError::Xdr("why_no_deleg truncated".to_string()));
}
let why = buf.get_u32();
match why {
1 | 2 => {
if buf.remaining() < 4 {
return Err(NfsError::Xdr("why_no_deleg bool truncated".to_string()));
}
buf.advance(4);
}
_ => {} }
}
_ => {
return Err(NfsError::Xdr(format!(
"unknown delegation type {}",
deleg_type
)));
}
}
Ok(())
}
fn skip_space_limit(buf: &mut Bytes) -> Result<()> {
if buf.remaining() < 4 {
return Err(NfsError::Xdr("space_limit type truncated".to_string()));
}
let limit_by = buf.get_u32();
match limit_by {
1 => {
if buf.remaining() < 8 {
return Err(NfsError::Xdr("space_limit size truncated".to_string()));
}
buf.advance(8);
}
2 => {
if buf.remaining() < 8 {
return Err(NfsError::Xdr("space_limit blocks truncated".to_string()));
}
buf.advance(8);
}
_ => return Err(NfsError::Xdr(format!("unknown limit_by {}", limit_by))),
}
Ok(())
}
fn skip_exchange_id_result(buf: &mut Bytes) -> Result<()> {
if buf.remaining() < 16 {
return Err(NfsError::Xdr("EXCHANGE_ID result truncated".to_string()));
}
buf.advance(16);
if buf.remaining() < 4 {
return Err(NfsError::Xdr("state_protect4_r type truncated".to_string()));
}
let sp = buf.get_u32();
match sp {
0 => {} 1 => {
skip_bitmap(buf)?;
skip_bitmap(buf)?;
}
2 => {
skip_bitmap(buf)?; skip_bitmap(buf)?; if buf.remaining() < 16 {
return Err(NfsError::Xdr("SP4_SSV fields truncated".to_string()));
}
buf.advance(16); if buf.remaining() < 4 {
return Err(NfsError::Xdr(
"SP4_SSV gss_handles len truncated".to_string(),
));
}
let nh = buf.get_u32() as usize;
if nh > 64 {
return Err(NfsError::Xdr(format!(
"SP4_SSV has {} gss_handles, max 64",
nh
)));
}
for _ in 0..nh {
skip_var_bytes(buf)?;
}
}
_ => {
return Err(NfsError::Xdr(format!(
"unsupported state_protect type {}",
sp
)));
}
}
if buf.remaining() < 8 {
return Err(NfsError::Xdr("server_owner minor_id truncated".to_string()));
}
buf.advance(8);
skip_var_bytes(buf)?;
skip_var_bytes(buf)?;
if buf.remaining() < 4 {
return Err(NfsError::Xdr("impl_id array len truncated".to_string()));
}
let n = buf.get_u32() as usize;
if n > 1 {
return Err(NfsError::Xdr(format!(
"nfs_impl_id4 has {} elements, max 1",
n
)));
}
for _ in 0..n {
skip_var_bytes(buf)?; skip_var_bytes(buf)?; if buf.remaining() < 12 {
return Err(NfsError::Xdr("impl_id time truncated".to_string()));
}
buf.advance(12);
}
Ok(())
}
fn skip_create_session_result(buf: &mut Bytes) -> Result<()> {
if buf.remaining() < 24 {
return Err(NfsError::Xdr("CREATE_SESSION result truncated".to_string()));
}
buf.advance(24);
for _ in 0..2 {
if buf.remaining() < 24 {
return Err(NfsError::Xdr("channel_attrs truncated".to_string()));
}
buf.advance(24); if buf.remaining() < 4 {
return Err(NfsError::Xdr("ca_rdma_ird length truncated".to_string()));
}
let n = buf.get_u32() as usize;
let bytes_needed = n
.checked_mul(4)
.ok_or_else(|| NfsError::Xdr("ca_rdma_ird count overflow".to_string()))?;
if buf.remaining() < bytes_needed {
return Err(NfsError::Xdr("ca_rdma_ird data truncated".to_string()));
}
buf.advance(bytes_needed);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn xdr_u32_encode() {
let mut buf = Vec::new();
xdr_u32(&mut buf, 0x12345678);
assert_eq!(buf, vec![0x12, 0x34, 0x56, 0x78]);
}
#[test]
fn xdr_u64_encode() {
let mut buf = Vec::new();
xdr_u64(&mut buf, 0x0102030405060708);
assert_eq!(buf, vec![0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]);
}
#[test]
fn xdr_bool_encode() {
let mut buf = Vec::new();
xdr_bool(&mut buf, true);
xdr_bool(&mut buf, false);
assert_eq!(buf, vec![0, 0, 0, 1, 0, 0, 0, 0]);
}
#[test]
fn xdr_var_bytes_with_padding() {
let mut buf = Vec::new();
xdr_var_bytes(&mut buf, &[0xAA, 0xBB, 0xCC]); assert_eq!(buf.len(), 4 + 4); assert_eq!(&buf[0..4], &3u32.to_be_bytes());
assert_eq!(&buf[4..7], &[0xAA, 0xBB, 0xCC]);
assert_eq!(buf[7], 0); }
#[test]
fn xdr_var_bytes_aligned() {
let mut buf = Vec::new();
xdr_var_bytes(&mut buf, &[1, 2, 3, 4]); assert_eq!(buf.len(), 8); }
#[test]
fn xdr_var_bytes_empty() {
let mut buf = Vec::new();
xdr_var_bytes(&mut buf, &[]);
assert_eq!(buf, vec![0, 0, 0, 0]); }
#[test]
fn xdr_string_encode() {
let mut buf = Vec::new();
xdr_string(&mut buf, "hi");
assert_eq!(buf.len(), 8); assert_eq!(&buf[0..4], &2u32.to_be_bytes());
assert_eq!(&buf[4..6], b"hi");
}
#[test]
fn xdr_bitmap_encode() {
let mut buf = Vec::new();
xdr_bitmap(&mut buf, &[0x1234, 0x5678]);
assert_eq!(buf.len(), 12); assert_eq!(&buf[0..4], &2u32.to_be_bytes());
assert_eq!(&buf[4..8], &0x1234u32.to_be_bytes());
assert_eq!(&buf[8..12], &0x5678u32.to_be_bytes());
}
#[test]
fn compound_builder_empty() {
let builder = CompoundBuilder::new("empty");
assert_eq!(builder.op_count(), 0);
let mut buf = Vec::new();
builder.encode_body(&mut buf);
assert_eq!(&buf[0..4], &5u32.to_be_bytes()); assert_eq!(&buf[4..9], b"empty");
let opcount_offset = 12 + 4; assert_eq!(
&buf[opcount_offset..opcount_offset + 4],
&0u32.to_be_bytes()
);
}
#[test]
fn compound_builder_putfh_encode() {
let fh = vec![0xDE, 0xAD, 0xBE, 0xEF];
let builder = CompoundBuilder::new("t").putfh(&fh);
assert_eq!(builder.op_count(), 1);
let mut buf = Vec::new();
builder.encode_body(&mut buf);
assert_eq!(&buf[16..20], &22u32.to_be_bytes()); assert_eq!(&buf[20..24], &4u32.to_be_bytes()); assert_eq!(&buf[24..28], &[0xDE, 0xAD, 0xBE, 0xEF]);
}
#[test]
fn compound_builder_lookup_encode() {
let builder = CompoundBuilder::new("t").lookup("test.txt");
let mut buf = Vec::new();
builder.encode_body(&mut buf);
assert_eq!(&buf[16..20], &15u32.to_be_bytes()); assert_eq!(&buf[20..24], &8u32.to_be_bytes()); assert_eq!(&buf[24..32], b"test.txt");
}
#[test]
fn compound_builder_read_encode() {
let stateid = [0u8; 16];
let builder = CompoundBuilder::new("t").read(&stateid, 1024, 4096);
let mut buf = Vec::new();
builder.encode_body(&mut buf);
assert_eq!(&buf[16..20], &25u32.to_be_bytes()); assert_eq!(&buf[36..44], &1024u64.to_be_bytes()); assert_eq!(&buf[44..48], &4096u32.to_be_bytes()); }
#[test]
fn compound_builder_chained_ops() {
let builder = CompoundBuilder::new("c")
.putrootfh()
.lookup("dir1")
.lookup("dir2")
.lookup("file")
.getfh()
.getattr(&[0, 0]);
assert_eq!(builder.op_count(), 6);
}
#[test]
fn compound_builder_write_encode() {
let stateid = [0xABu8; 16];
let data = [1u8, 2, 3, 4, 5];
let builder = CompoundBuilder::new("t").write(&stateid, 0, 2, &data);
let mut buf = Vec::new();
builder.encode_body(&mut buf);
assert_eq!(&buf[16..20], &38u32.to_be_bytes()); }
#[test]
fn open_delegation_preferences_match_linux_wire_values() {
let encode_access = |want_no_delegation| {
let args = OpenArgs {
seqid: 0,
share_access: 3,
share_deny: 0,
client_id: 1,
owner: Bytes::new(),
create: false,
create_attrs_mask: Vec::new(),
create_attrs_vals: Vec::new(),
claim_file: "f".to_string(),
want_no_delegation,
};
let mut encoded = Vec::new();
args.encode(&mut encoded);
u32::from_be_bytes(encoded[4..8].try_into().unwrap())
};
assert_eq!(encode_access(true), 0x0403);
assert_eq!(encode_access(false), 0x0203);
}
#[test]
fn compound_builder_remove_encode() {
let builder = CompoundBuilder::new("t").remove("oldfile");
let mut buf = Vec::new();
builder.encode_body(&mut buf);
assert_eq!(&buf[16..20], &28u32.to_be_bytes()); }
#[test]
fn compound_builder_rename_encode() {
let builder = CompoundBuilder::new("t").rename("old", "new");
assert_eq!(builder.op_count(), 1);
}
#[test]
fn compound_builder_create_symlink_encode() {
let builder = CompoundBuilder::new("t").create_symlink("link", "/target/path", &[], &[]);
assert_eq!(builder.op_count(), 1);
let mut buf = Vec::new();
builder.encode_body(&mut buf);
assert_eq!(&buf[16..20], &6u32.to_be_bytes()); assert_eq!(&buf[20..24], &5u32.to_be_bytes());
}
#[test]
fn compound_response_decode_simple_ok() {
let mut buf = Vec::new();
xdr_u32(&mut buf, 0); xdr_string(&mut buf, ""); xdr_u32(&mut buf, 1); xdr_u32(&mut buf, OpNum::PutRootFh as u32); xdr_u32(&mut buf, 0);
let resp = CompoundResponse::decode(Bytes::from(buf)).unwrap();
assert!(matches!(resp.status, nfsstat4::NFS4_OK));
assert_eq!(resp.results.len(), 1);
assert_eq!(resp.results[0].opcode, OpNum::PutRootFh as u32);
}
#[test]
fn compound_response_decode_with_sequence() {
let mut buf = Vec::new();
xdr_u32(&mut buf, 0); xdr_string(&mut buf, "test");
xdr_u32(&mut buf, 2);
xdr_u32(&mut buf, OpNum::Sequence as u32);
xdr_u32(&mut buf, 0); buf.extend_from_slice(&[0u8; 16]); xdr_u32(&mut buf, 1); xdr_u32(&mut buf, 0); xdr_u32(&mut buf, 3); xdr_u32(&mut buf, 3); xdr_u32(&mut buf, 0);
xdr_u32(&mut buf, OpNum::PutRootFh as u32);
xdr_u32(&mut buf, 0);
let resp = CompoundResponse::decode(Bytes::from(buf)).unwrap();
assert_eq!(resp.tag, "test");
assert_eq!(resp.results.len(), 2);
resp.op_ok(0).unwrap(); resp.op_ok(1).unwrap(); }
#[test]
fn compound_response_decode_error_status() {
let mut buf = Vec::new();
xdr_u32(&mut buf, 2); xdr_string(&mut buf, "");
xdr_u32(&mut buf, 0);
let resp = CompoundResponse::decode(Bytes::from(buf)).unwrap();
assert!(matches!(resp.status, nfsstat4::NFS4ERR_NOENT));
assert!(resp.check_status().is_err());
}
#[test]
fn compound_response_op_ok_out_of_bounds() {
let mut buf = Vec::new();
xdr_u32(&mut buf, 0);
xdr_string(&mut buf, "");
xdr_u32(&mut buf, 0);
let resp = CompoundResponse::decode(Bytes::from(buf)).unwrap();
assert!(resp.op_ok(0).is_err()); }
#[test]
fn compound_response_decode_getfh_ok() {
let mut buf = Vec::new();
xdr_u32(&mut buf, 0); xdr_string(&mut buf, "");
xdr_u32(&mut buf, 1); xdr_u32(&mut buf, OpNum::GetFh as u32);
xdr_u32(&mut buf, 0); xdr_var_bytes(&mut buf, &[0xDE, 0xAD]);
let resp = CompoundResponse::decode(Bytes::from(buf)).unwrap();
let op = resp.op_ok(0).unwrap();
let mut data = op.data.clone();
let len = data.get_u32() as usize;
assert_eq!(len, 2);
}
#[test]
fn compound_response_decode_truncated() {
let buf = vec![0u8; 2]; assert!(CompoundResponse::decode(Bytes::from(buf)).is_err());
}
#[test]
fn compound_builder_basic_encode() {
let builder = CompoundBuilder::new("test")
.putrootfh()
.lookup("mydir")
.getfh()
.getattr(&[0x0018_00bb, 0x0030_0b73]);
assert_eq!(builder.op_count(), 4);
let mut buf = Vec::new();
builder.encode_body(&mut buf);
assert_eq!(&buf[0..4], &4u32.to_be_bytes()); assert_eq!(&buf[4..8], b"test");
assert_eq!(&buf[8..12], &1u32.to_be_bytes());
assert_eq!(&buf[12..16], &4u32.to_be_bytes());
assert_eq!(&buf[16..20], &24u32.to_be_bytes());
}
#[test]
fn compound_builder_sequence_encode() {
let session_id = [1u8; 16];
let builder = CompoundBuilder::new("seq").sequence(&session_id, 42, 0, 0);
let mut buf = Vec::new();
builder.encode_body(&mut buf);
let op_start = 16; assert_eq!(
&buf[op_start..op_start + 4],
&(OpNum::Sequence as u32).to_be_bytes()
);
assert_eq!(&buf[op_start + 4..op_start + 20], &session_id);
assert_eq!(&buf[op_start + 20..op_start + 24], &42u32.to_be_bytes());
}
#[test]
fn negotiated_operation_limit_is_enforced_at_boundaries() {
let session_id = [0u8; 16];
let one = CompoundBuilder::new("one").sequence(&session_id, 1, 0, 0);
assert!(one.enforce_max_operations(0).is_err());
assert!(one.enforce_max_operations(1).is_ok());
let two = CompoundBuilder::new("two")
.sequence(&session_id, 1, 0, 0)
.putrootfh();
assert!(two.enforce_max_operations(1).is_err());
assert!(two.enforce_max_operations(2).is_ok());
assert!(two.enforce_max_operations(3).is_ok());
}
#[test]
fn operation_classes_cover_supported_opnums() {
let read_only = [
OpNum::Access,
OpNum::GetAttr,
OpNum::GetFh,
OpNum::GetDeviceInfo,
OpNum::Lockt,
OpNum::Lookup,
OpNum::Lookupp,
OpNum::PutFh,
OpNum::PutRootFh,
OpNum::Read,
OpNum::ReadDir,
OpNum::ReadLink,
OpNum::RestoreFh,
OpNum::SaveFh,
OpNum::TestStateId,
];
let control = [
OpNum::BindConnToSession,
OpNum::CreateSession,
OpNum::DestroyClientId,
OpNum::DestroySession,
OpNum::ExchangeId,
OpNum::ReclaimComplete,
OpNum::Sequence,
];
let replay_sensitive = [
OpNum::Close,
OpNum::Commit,
OpNum::Create,
OpNum::DelegReturn,
OpNum::FreeStateId,
OpNum::LayoutCommit,
OpNum::LayoutGet,
OpNum::LayoutReturn,
OpNum::Link,
OpNum::Lock,
OpNum::Locku,
OpNum::Open,
OpNum::OpenAttr,
OpNum::Remove,
OpNum::Rename,
OpNum::SetAttr,
OpNum::Write,
];
assert!(
read_only
.iter()
.all(|op| op.class() == OperationClass::ReadOnly)
);
assert!(
control
.iter()
.all(|op| op.class() == OperationClass::SessionControl)
);
assert!(
replay_sensitive
.iter()
.all(|op| op.class() == OperationClass::ReplaySensitive)
);
assert_eq!(read_only.len() + control.len() + replay_sensitive.len(), 39);
}
#[test]
fn cache_policy_is_false_for_read_only_compound() {
let session_id = [1u8; 16];
let builder = CompoundBuilder::new("read")
.sequence(&session_id, 1, 0, 0)
.putrootfh()
.lookup("file")
.getattr(&[1])
.apply_sequence_cache_policy(0)
.unwrap();
assert_eq!(
&builder.ops[0].args[SEQUENCE_CACHE_THIS_OFFSET..SEQUENCE_CACHE_THIS_OFFSET + 4],
&0u32.to_be_bytes()
);
}
#[test]
fn cache_policy_is_true_for_mixed_modifying_compound() {
let session_id = [1u8; 16];
let stateid = [0u8; 16];
let builder = CompoundBuilder::new("write")
.sequence(&session_id, 1, 0, 0)
.putfh(b"fh")
.write_header(&stateid, 0, 2, 1_048_576)
.getattr(&[1])
.apply_sequence_cache_policy(2128)
.unwrap();
assert_eq!(
&builder.ops[0].args[SEQUENCE_CACHE_THIS_OFFSET..SEQUENCE_CACHE_THIS_OFFSET + 4],
&1u32.to_be_bytes()
);
assert_eq!(builder.ops[2].args.len(), 32);
assert_eq!(&builder.ops[2].args[28..32], &1_048_576u32.to_be_bytes());
}
#[test]
fn cache_policy_checks_negotiated_capacity_boundary_before_encode() {
let session_id = [1u8; 16];
let builder_at_limit = CompoundBuilder::new("remove")
.sequence(&session_id, 1, 0, 0)
.putfh(b"fh")
.remove("file");
let required = builder_at_limit.minimum_cached_response_size();
assert!(
builder_at_limit
.apply_sequence_cache_policy(required)
.is_ok()
);
let builder_below_limit = CompoundBuilder::new("remove")
.sequence(&session_id, 1, 0, 0)
.putfh(b"fh")
.remove("file");
let error = match builder_below_limit.apply_sequence_cache_policy(required - 1) {
Ok(_) => panic!("modifying compound requires cached reply capacity"),
Err(error) => error,
};
assert!(
error
.to_string()
.contains(&format!("requires at least {required}"))
);
}
#[test]
fn cache_policy_requires_sequence_first() {
let result = CompoundBuilder::new("invalid")
.putrootfh()
.apply_sequence_cache_policy(2128);
let error = match result {
Ok(_) => panic!("missing SEQUENCE must fail"),
Err(error) => error,
};
assert!(error.to_string().contains("not a valid SEQUENCE"));
}
#[test]
fn required_generation_is_preserved_by_builder_chaining() {
let builder = CompoundBuilder::new("generation-fence")
.require_generation(7)
.putrootfh()
.getattr(&[1]);
assert_eq!(builder.required_generation(), Some(7));
}
#[test]
fn anonymous_stateid_generation_is_not_fenced() {
let builder = CompoundBuilder::new("anonymous")
.require_generation(0)
.putrootfh();
assert_eq!(builder.required_generation(), None);
}
}