1mod access;
18mod commit;
19mod create;
20mod export;
21mod fsinfo;
22mod fsstat;
23mod getattr;
24mod link;
25mod lookup;
26mod mkdir;
27mod mount;
28mod null;
29mod pathconf;
30mod read;
31mod readdir;
32mod readdirplus;
33mod readlink;
34mod remove;
35mod rename;
36mod rmdir;
37mod setattr;
38mod symlink;
39mod umount;
40mod write;
41
42pub(crate) use mount::{mount, query_exports};
43
44use crate::error::{NfsError, RequestContext, Result, classify_sent_nfs3_error};
45use crate::{Auth, ObjRes, Time, rpc};
46use bytes::Bytes;
47
48pub(crate) fn bytes_to_string(raw: Bytes) -> String {
51 match std::str::from_utf8(&raw) {
52 Ok(s) => s.to_owned(),
53 Err(_) => {
54 tracing::warn!(raw_hex = %hex_preview(&raw), "NFS name contains invalid UTF-8, using lossy conversion");
55 String::from_utf8_lossy(&raw).into_owned()
56 }
57 }
58}
59
60fn hex_preview(bytes: &[u8]) -> String {
61 let limit = bytes.len().min(32);
62 let hex: String = bytes[..limit]
63 .iter()
64 .map(|b| format!("{:02x}", b))
65 .collect::<Vec<_>>()
66 .join(" ");
67 if bytes.len() > 32 {
68 format!("{}...", hex)
69 } else {
70 hex
71 }
72}
73
74pub(crate) mod fastxdr;
75
76pub(crate) use fastxdr::{
80 ACCESS3resok, COMMIT3resok, CREATE3resok, FSINFO3resok, FSSTAT3resok, GETATTR3resok,
81 LINK3resok, LOOKUP3resok, MKDIR3resok, PATHCONF3resok, READ3resok, READDIR3resok,
82 READDIRPLUS3resok, READLINK3resok, REMOVE3resok, RENAME3resok, RMDIR3resok, SETATTR3resok,
83 SYMLINK3resok, WRITE3resok, entry3, entryplus3, fattr3, mount_mountstat3, mountres3_ok,
84 nfsstat3, nfstime3, post_op_attr, post_op_fh3, stable_how,
85};
86
87macro_rules! paged_dir_stream {
100 ($self:expr_2021, $dir_fh:expr_2021, $fetch_page:ident, $convert:expr_2021, $label:literal) => {{
101 let this = $self;
102 futures::stream::try_unfold(
103 Some(($dir_fh, crate::mount::DirectoryCursor::default())),
104 move |state| async move {
105 let Some((fh, mut cursor)) = state else {
106 return Ok::<_, crate::error::NfsError>(None);
107 };
108 let cookie = cursor.cookie;
109 let res = this
110 .$fetch_page(fh.clone(), cookie, cursor.verifier)
111 .await?;
112 let new_verifier: [u8; 8] =
113 res.cookieverf.0.as_ref().try_into().unwrap_or([0u8; 8]);
114 let eof = res.reply.eof;
115 let (new_cookie, entry_count, entries_head) = match res.reply.entries {
117 Some(entry) => {
118 let mut count = 0usize;
119 let mut e = &*entry;
120 let last_cookie = loop {
121 count += 1;
122 match &e.nextentry {
123 Some(next) => e = next,
124 None => break e.cookie.0,
125 }
126 };
127 (last_cookie, count, Some(entry))
128 }
129 None => (cookie, 0, None),
130 };
131 tracing::debug!(cookie = new_cookie, eof, entry_count, $label);
132 let next = if cursor.advance(new_cookie, new_verifier, entry_count, eof)? {
133 Some((fh, cursor))
134 } else {
135 None
136 };
137 let convert = $convert;
139 let entry_iter = {
140 let mut current = entries_head;
141 std::iter::from_fn(move || {
142 loop {
143 let mut node = current.take()?;
144 current = node.nextentry.take();
145 let name = node.name.0.as_ref();
146 if name != b"." && name != b".." {
147 return Some(Ok(convert(node)));
148 }
149 }
150 })
151 };
152 Ok(Some((futures::stream::iter(entry_iter), next)))
153 },
154 )
155 .try_flatten()
156 }};
157}
158
159pub(crate) use paged_dir_stream;
160
161#[allow(dead_code)]
162enum MountProc3 {
163 Null = 0,
164 Mount = 1,
165 Umount = 3,
166 Export = 5,
167}
168
169#[derive(Clone, Copy)]
170enum NFSProc3 {
171 Null = 0,
172 GetAttr = 1,
173 SetAttr = 2,
174 Lookup = 3,
175 Access = 4,
176 Readlink = 5,
177 Read = 6,
178 Write = 7,
179 Create = 8,
180 Mkdir = 9,
181 Symlink = 10,
182 Remove = 12,
183 Rmdir = 13,
184 Rename = 14,
185 Link = 15,
186 Readdir = 16,
187 Readdirplus = 17,
188 FSStat = 18,
189 FSInfo = 19,
190 Pathconf = 20,
191 Commit = 21,
192}
193
194impl NFSProc3 {
195 const fn metadata(&self) -> (&'static str, crate::OperationClass) {
196 match self {
197 Self::Null => ("null", crate::OperationClass::ReadOnly),
198 Self::GetAttr => ("getattr", crate::OperationClass::ReadOnly),
199 Self::SetAttr => ("setattr", crate::OperationClass::ReplaySensitive),
200 Self::Lookup => ("lookup", crate::OperationClass::ReadOnly),
201 Self::Access => ("access", crate::OperationClass::ReadOnly),
202 Self::Readlink => ("readlink", crate::OperationClass::ReadOnly),
203 Self::Read => ("read", crate::OperationClass::ReadOnly),
204 Self::Write => ("write", crate::OperationClass::ReplaySensitive),
205 Self::Create => ("create", crate::OperationClass::ReplaySensitive),
206 Self::Mkdir => ("mkdir", crate::OperationClass::ReplaySensitive),
207 Self::Symlink => ("symlink", crate::OperationClass::ReplaySensitive),
208 Self::Remove => ("remove", crate::OperationClass::ReplaySensitive),
209 Self::Rmdir => ("rmdir", crate::OperationClass::ReplaySensitive),
210 Self::Rename => ("rename", crate::OperationClass::ReplaySensitive),
211 Self::Link => ("link", crate::OperationClass::ReplaySensitive),
212 Self::Readdir => ("readdir", crate::OperationClass::ReadOnly),
213 Self::Readdirplus => ("readdirplus", crate::OperationClass::ReadOnly),
214 Self::FSStat => ("fsstat", crate::OperationClass::ReadOnly),
215 Self::FSInfo => ("fsinfo", crate::OperationClass::ReadOnly),
216 Self::Pathconf => ("pathconf", crate::OperationClass::ReadOnly),
217 Self::Commit => ("commit", crate::OperationClass::ReplaySensitive),
218 }
219 }
220
221 const fn operation_class(&self) -> crate::OperationClass {
222 self.metadata().1
223 }
224
225 const fn operation_name(&self) -> &'static str {
226 self.metadata().0
227 }
228
229 const fn replay_policy(&self) -> crate::rpc::ReplayPolicy {
230 match self.operation_class() {
231 crate::OperationClass::ReadOnly => NFS_REPLAY,
232 crate::OperationClass::SessionControl | crate::OperationClass::ReplaySensitive => {
233 crate::rpc::ReplayPolicy::ONE_ATTEMPT
234 }
235 }
236 }
237
238 fn request_context(&self) -> RequestContext {
239 RequestContext {
240 operation: self.operation_name().to_string(),
241 protocol: crate::NFSVersion::NFSv3,
242 request_id: None,
243 }
244 }
245}
246
247#[cfg(test)]
248mod operation_class_tests {
249 use super::NFSProc3;
250 use crate::OperationClass;
251
252 #[test]
253 fn every_supported_procedure_has_an_explicit_operation_class() {
254 let cases = [
255 (NFSProc3::Null, OperationClass::ReadOnly),
256 (NFSProc3::GetAttr, OperationClass::ReadOnly),
257 (NFSProc3::SetAttr, OperationClass::ReplaySensitive),
258 (NFSProc3::Lookup, OperationClass::ReadOnly),
259 (NFSProc3::Access, OperationClass::ReadOnly),
260 (NFSProc3::Readlink, OperationClass::ReadOnly),
261 (NFSProc3::Read, OperationClass::ReadOnly),
262 (NFSProc3::Write, OperationClass::ReplaySensitive),
263 (NFSProc3::Create, OperationClass::ReplaySensitive),
264 (NFSProc3::Mkdir, OperationClass::ReplaySensitive),
265 (NFSProc3::Symlink, OperationClass::ReplaySensitive),
266 (NFSProc3::Remove, OperationClass::ReplaySensitive),
267 (NFSProc3::Rmdir, OperationClass::ReplaySensitive),
268 (NFSProc3::Rename, OperationClass::ReplaySensitive),
269 (NFSProc3::Link, OperationClass::ReplaySensitive),
270 (NFSProc3::Readdir, OperationClass::ReadOnly),
271 (NFSProc3::Readdirplus, OperationClass::ReadOnly),
272 (NFSProc3::FSStat, OperationClass::ReadOnly),
273 (NFSProc3::FSInfo, OperationClass::ReadOnly),
274 (NFSProc3::Pathconf, OperationClass::ReadOnly),
275 (NFSProc3::Commit, OperationClass::ReplaySensitive),
276 ];
277
278 for (procedure, expected) in cases {
279 assert_eq!(procedure.operation_class(), expected);
280 }
281 }
282
283 #[test]
284 fn replay_sensitive_procedures_never_use_transport_replay() {
285 assert_eq!(
286 NFSProc3::Write.replay_policy(),
287 crate::rpc::ReplayPolicy::ONE_ATTEMPT
288 );
289 assert_eq!(
290 NFSProc3::Rename.replay_policy(),
291 crate::rpc::ReplayPolicy::ONE_ATTEMPT
292 );
293 assert_eq!(NFSProc3::Read.replay_policy(), super::NFS_REPLAY);
294 }
295}
296
297trait XdrEncode {
299 fn encode(&self, buf: &mut Vec<u8>);
300}
301
302fn xdr_u32(buf: &mut Vec<u8>, v: u32) {
304 buf.extend_from_slice(&v.to_be_bytes());
305}
306
307fn xdr_u64(buf: &mut Vec<u8>, v: u64) {
308 buf.extend_from_slice(&v.to_be_bytes());
309}
310
311fn xdr_i32(buf: &mut Vec<u8>, v: i32) {
312 buf.extend_from_slice(&v.to_be_bytes());
313}
314
315fn xdr_fixed_bytes(buf: &mut Vec<u8>, data: &[u8]) {
317 buf.extend_from_slice(data);
318 let pad = (4 - data.len() % 4) % 4;
319 for _ in 0..pad {
320 buf.push(0);
321 }
322}
323
324fn xdr_var_bytes(buf: &mut Vec<u8>, data: &[u8]) {
326 xdr_u32(buf, data.len() as u32);
327 xdr_fixed_bytes(buf, data);
328}
329
330fn xdr_string(buf: &mut Vec<u8>, s: &str) {
332 xdr_var_bytes(buf, s.as_bytes());
333}
334
335const MOUNT_RETRIES: usize = 2;
337const NFS_RETRIES: usize = 10;
339const MOUNT_REPLAY: crate::rpc::ReplayPolicy =
340 crate::rpc::ReplayPolicy::byte_identical(MOUNT_RETRIES);
341const NFS_REPLAY: crate::rpc::ReplayPolicy = crate::rpc::ReplayPolicy::byte_identical(NFS_RETRIES);
342const METADATA_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
344const DATA_TIMEOUT_BASE_SECS: u64 = 10;
346const MIN_BANDWIDTH_BYTES_PER_SEC: u64 = 1_250_000;
348
349fn data_timeout(data_size: usize) -> std::time::Duration {
350 let transfer_secs = data_size as u64 / MIN_BANDWIDTH_BYTES_PER_SEC;
351 std::time::Duration::from_secs(DATA_TIMEOUT_BASE_SECS + transfer_secs)
352}
353
354macro_rules! nfs3_call {
364 ($name:ident, $proc:ident, $args:ty, $resok:ty) => {
365 nfs3_call!($name, $proc, $args, $resok, warn);
366 };
367 ($name:ident, $proc:ident, $args:ty, $resok:ty, $err_level:ident) => {
368 async fn $name(&self, args: $args) -> Result<$resok> {
369 let procedure = NFSProc3::$proc;
370 let operation_class = procedure.operation_class();
371 let context = procedure.request_context();
372 let mut buf = Vec::<u8>::new();
373 self.pack_nfs3(procedure, &args, &mut buf);
374 tracing::debug!(proc = stringify!($proc), "NFS3 call");
375 let mut bytes = self
376 .rpc
377 .call(
378 buf,
379 procedure.replay_policy(),
380 METADATA_TIMEOUT,
381 )
382 .await
383 .map_err(|error| classify_sent_nfs3_error(operation_class, context.clone(), error))?;
384 let status = nfsstat3::try_from(&mut bytes)
385 .map_err(|error| classify_sent_nfs3_error(operation_class, context.clone(), NfsError::Xdr(error.to_string())))?;
386 match status {
387 nfsstat3::NFS3_OK => {
388 tracing::trace!(proc = stringify!($proc), "NFS3 call succeeded");
389 <$resok>::try_from(&mut bytes)
390 .map_err(|error| classify_sent_nfs3_error(operation_class, context, NfsError::Xdr(error.to_string())))
391 }
392 e => {
393 tracing::$err_level!(proc = stringify!($proc), status = ?e, "NFS3 call returned error status");
394 Err(NfsError::Nfs3(e))
395 }
396 }
397 }
398 };
399}
400
401#[derive(Debug)]
404pub struct Mount {
405 pub(crate) rpc: rpc::Client,
406 pub(crate) auth: Auth,
407 pub(crate) fh: Bytes,
408 pub(crate) dir: String,
409 pub(crate) dircount: u32,
410 pub(crate) maxcount: u32,
411 pub(crate) rsize: u32,
412 pub(crate) wsize: u32,
413}
414
415impl Mount {
416 fn pack_nfs3(&self, proc: NFSProc3, args: &dyn XdrEncode, buf: &mut Vec<u8>) {
417 rpc_header(rpc::NFS_PROG, rpc::NFS3_VERSION, proc as u32, &self.auth).encode(buf);
418 args.encode(buf);
419 }
420
421 pub fn getfh(&self) -> Bytes {
422 self.fh.clone()
423 }
424
425 async fn _null(&self, args: NULL3args) -> Result<()> {
427 let procedure = NFSProc3::Null;
428 let mut buf = Vec::<u8>::new();
429 self.pack_nfs3(procedure, &args, &mut buf);
430 tracing::debug!(proc = "Null", "NFS3 call");
431 let _ = self
432 .rpc
433 .call(buf, procedure.replay_policy(), METADATA_TIMEOUT)
434 .await
435 .map_err(|error| {
436 classify_sent_nfs3_error(
437 procedure.operation_class(),
438 procedure.request_context(),
439 error,
440 )
441 })?;
442 tracing::trace!(proc = "Null", "NFS3 call succeeded");
443 Ok(())
444 }
445
446 nfs3_call!(_access, Access, ACCESS3args, ACCESS3resok);
447 nfs3_call!(_commit, Commit, COMMIT3args, COMMIT3resok);
448 nfs3_call!(_create, Create, CREATE3args, CREATE3resok);
449 nfs3_call!(_fsinfo, FSInfo, FSINFO3args, FSINFO3resok);
450 nfs3_call!(_fsstat, FSStat, FSSTAT3args, FSSTAT3resok);
451 nfs3_call!(_getattr, GetAttr, GETATTR3args, GETATTR3resok);
452 nfs3_call!(_link, Link, LINK3args, LINK3resok);
453 nfs3_call!(_lookup, Lookup, LOOKUP3args, LOOKUP3resok);
454 nfs3_call!(_mkdir, Mkdir, MKDIR3args, MKDIR3resok, info);
455 nfs3_call!(_pathconf, Pathconf, PATHCONF3args, PATHCONF3resok);
456 async fn _read(&self, args: READ3args) -> Result<READ3resok> {
458 let procedure = NFSProc3::Read;
459 let operation_class = procedure.operation_class();
460 let context = procedure.request_context();
461 let timeout = data_timeout(args.count as usize);
462 let mut buf = Vec::<u8>::new();
463 self.pack_nfs3(procedure, &args, &mut buf);
464 tracing::debug!(proc = "Read", "NFS3 call");
465 let mut bytes = self
466 .rpc
467 .call(buf, procedure.replay_policy(), timeout)
468 .await
469 .map_err(|error| classify_sent_nfs3_error(operation_class, context.clone(), error))?;
470 let status = nfsstat3::try_from(&mut bytes).map_err(|error| {
471 classify_sent_nfs3_error(
472 operation_class,
473 context.clone(),
474 NfsError::Xdr(error.to_string()),
475 )
476 })?;
477 match status {
478 nfsstat3::NFS3_OK => {
479 tracing::trace!(proc = "Read", "NFS3 call succeeded");
480 READ3resok::try_from(&mut bytes).map_err(|error| {
481 classify_sent_nfs3_error(
482 operation_class,
483 context,
484 NfsError::Xdr(error.to_string()),
485 )
486 })
487 }
488 e => {
489 tracing::warn!(proc = "Read", status = ?e, "NFS3 call returned error status");
490 Err(NfsError::Nfs3(e))
491 }
492 }
493 }
494 nfs3_call!(_readdir, Readdir, READDIR3args, READDIR3resok);
495 nfs3_call!(
496 _readdirplus,
497 Readdirplus,
498 READDIRPLUS3args,
499 READDIRPLUS3resok
500 );
501 nfs3_call!(_readlink, Readlink, READLINK3args, READLINK3resok);
502 nfs3_call!(_remove, Remove, REMOVE3args, REMOVE3resok);
503 nfs3_call!(_rename, Rename, RENAME3args, RENAME3resok);
504 nfs3_call!(_rmdir, Rmdir, RMDIR3args, RMDIR3resok);
505 nfs3_call!(_setattr, SetAttr, SETATTR3args, SETATTR3resok);
506 nfs3_call!(_symlink, Symlink, SYMLINK3args, SYMLINK3resok);
507 async fn _write(&self, args: WRITE3args) -> Result<WRITE3resok> {
511 let procedure = NFSProc3::Write;
512 let operation_class = procedure.operation_class();
513 let context = procedure.request_context();
514 let data = args.data.clone();
515 let timeout = data_timeout(data.len());
516 let mut buf = Vec::<u8>::new();
517 self.pack_nfs3(procedure, &args, &mut buf);
518 tracing::debug!(proc = "Write", data_len = data.len(), "NFS3 call");
519 let mut bytes = self
520 .rpc
521 .call_with_data(buf, data, procedure.replay_policy(), timeout)
522 .await
523 .map_err(|error| classify_sent_nfs3_error(operation_class, context.clone(), error))?;
524 let status = nfsstat3::try_from(&mut bytes).map_err(|error| {
525 classify_sent_nfs3_error(
526 operation_class,
527 context.clone(),
528 NfsError::Xdr(error.to_string()),
529 )
530 })?;
531 match status {
532 nfsstat3::NFS3_OK => {
533 tracing::trace!(proc = "Write", "NFS3 call succeeded");
534 WRITE3resok::try_from(&mut bytes).map_err(|error| {
535 classify_sent_nfs3_error(
536 operation_class,
537 context,
538 NfsError::Xdr(error.to_string()),
539 )
540 })
541 }
542 e => {
543 tracing::warn!(proc = "Write", status = ?e, "NFS3 call returned error status");
544 Err(NfsError::Nfs3(e))
545 }
546 }
547 }
548}
549
550pub(crate) fn rpc_header(prog: u32, vers: u32, proc: u32, cred: &Auth) -> rpc::Header {
551 rpc::Header::new(rpc::RPC_VERSION, prog, vers, proc, cred, &Auth::new_null())
552}
553
554#[allow(non_camel_case_types)]
559#[derive(Debug, PartialEq)]
560pub(crate) struct nfs_fh3 {
561 pub(crate) data: Bytes,
562}
563
564impl XdrEncode for nfs_fh3 {
565 fn encode(&self, buf: &mut Vec<u8>) {
566 xdr_var_bytes(buf, &self.data);
567 }
568}
569
570#[allow(non_camel_case_types)]
571#[derive(Debug, PartialEq)]
572pub(crate) struct filename3(pub(crate) String);
573
574impl XdrEncode for filename3 {
575 fn encode(&self, buf: &mut Vec<u8>) {
576 xdr_string(buf, &self.0);
577 }
578}
579
580#[allow(non_camel_case_types)]
581#[derive(Debug, PartialEq)]
582pub(crate) struct nfspath3(pub(crate) String);
583
584impl XdrEncode for nfspath3 {
585 fn encode(&self, buf: &mut Vec<u8>) {
586 xdr_string(buf, &self.0);
587 }
588}
589
590#[allow(non_camel_case_types)]
591#[derive(Debug, PartialEq)]
592pub(crate) struct diropargs3 {
593 pub(crate) dir: nfs_fh3,
594 pub(crate) name: filename3,
595}
596
597impl XdrEncode for diropargs3 {
598 fn encode(&self, buf: &mut Vec<u8>) {
599 self.dir.encode(buf);
600 self.name.encode(buf);
601 }
602}
603
604#[allow(non_camel_case_types, clippy::upper_case_acronyms)]
606#[derive(Debug, PartialEq)]
607pub(crate) enum set_mode3 {
608 TRUE(u32),
609 default,
610}
611
612#[allow(non_camel_case_types, clippy::upper_case_acronyms)]
613#[derive(Debug, PartialEq)]
614pub(crate) enum set_uid3 {
615 TRUE(u32),
616 default,
617}
618
619#[allow(non_camel_case_types, clippy::upper_case_acronyms)]
620#[derive(Debug, PartialEq)]
621pub(crate) enum set_gid3 {
622 TRUE(u32),
623 default,
624}
625
626#[allow(non_camel_case_types, clippy::upper_case_acronyms)]
627#[derive(Debug, PartialEq)]
628pub(crate) enum set_size3 {
629 TRUE(u64),
630 default,
631}
632
633#[allow(non_camel_case_types)]
634#[derive(Debug, PartialEq)]
635pub(crate) enum set_atime {
636 SET_TO_CLIENT_TIME(nfstime3_req),
637 default,
638}
639
640#[allow(non_camel_case_types)]
641#[derive(Debug, PartialEq)]
642pub(crate) enum set_mtime {
643 SET_TO_CLIENT_TIME(nfstime3_req),
644 default,
645}
646
647#[allow(non_camel_case_types)]
649#[derive(Debug, PartialEq)]
650pub(crate) struct nfstime3_req {
651 pub(crate) seconds: u32,
652 pub(crate) nseconds: u32,
653}
654
655#[allow(non_camel_case_types)]
656#[derive(Debug, PartialEq)]
657pub(crate) struct sattr3 {
658 pub(crate) mode: set_mode3,
659 pub(crate) uid: set_uid3,
660 pub(crate) gid: set_gid3,
661 pub(crate) size: set_size3,
662 pub(crate) atime: set_atime,
663 pub(crate) mtime: set_mtime,
664}
665
666impl Default for sattr3 {
667 fn default() -> Self {
668 Self {
669 mode: set_mode3::default,
670 uid: set_uid3::default,
671 gid: set_gid3::default,
672 size: set_size3::default,
673 atime: set_atime::default,
674 mtime: set_mtime::default,
675 }
676 }
677}
678
679impl XdrEncode for sattr3 {
680 fn encode(&self, buf: &mut Vec<u8>) {
681 match &self.mode {
683 set_mode3::TRUE(v) => {
684 xdr_i32(buf, 1);
685 xdr_u32(buf, *v);
686 }
687 set_mode3::default => xdr_i32(buf, 0),
688 }
689 match &self.uid {
691 set_uid3::TRUE(v) => {
692 xdr_i32(buf, 1);
693 xdr_u32(buf, *v);
694 }
695 set_uid3::default => xdr_i32(buf, 0),
696 }
697 match &self.gid {
699 set_gid3::TRUE(v) => {
700 xdr_i32(buf, 1);
701 xdr_u32(buf, *v);
702 }
703 set_gid3::default => xdr_i32(buf, 0),
704 }
705 match &self.size {
707 set_size3::TRUE(v) => {
708 xdr_i32(buf, 1);
709 xdr_u64(buf, *v);
710 }
711 set_size3::default => xdr_i32(buf, 0),
712 }
713 match &self.atime {
715 set_atime::SET_TO_CLIENT_TIME(t) => {
716 xdr_i32(buf, 2); xdr_u32(buf, t.seconds);
718 xdr_u32(buf, t.nseconds);
719 }
720 set_atime::default => xdr_i32(buf, 0),
721 }
722 match &self.mtime {
724 set_mtime::SET_TO_CLIENT_TIME(t) => {
725 xdr_i32(buf, 2); xdr_u32(buf, t.seconds);
727 xdr_u32(buf, t.nseconds);
728 }
729 set_mtime::default => xdr_i32(buf, 0),
730 }
731 }
732}
733
734#[allow(non_camel_case_types, clippy::upper_case_acronyms)]
735#[derive(Debug, PartialEq)]
736pub(crate) enum sattrguard3 {
737 TRUE(nfstime3_req),
738 FALSE,
739}
740
741impl XdrEncode for sattrguard3 {
742 fn encode(&self, buf: &mut Vec<u8>) {
743 match self {
744 sattrguard3::TRUE(t) => {
745 xdr_i32(buf, 1);
746 xdr_u32(buf, t.seconds);
747 xdr_u32(buf, t.nseconds);
748 }
749 sattrguard3::FALSE => xdr_i32(buf, 0),
750 }
751 }
752}
753
754#[allow(non_camel_case_types)]
755#[derive(Debug, PartialEq)]
756pub(crate) struct symlinkdata3 {
757 pub(crate) symlink_attributes: sattr3,
758 pub(crate) symlink_data: nfspath3,
759}
760
761impl XdrEncode for symlinkdata3 {
762 fn encode(&self, buf: &mut Vec<u8>) {
763 self.symlink_attributes.encode(buf);
764 self.symlink_data.encode(buf);
765 }
766}
767
768#[allow(non_camel_case_types, dead_code, clippy::upper_case_acronyms)]
769#[derive(Debug, PartialEq)]
770pub(crate) enum createhow3 {
771 UNCHECKED(sattr3),
772 GUARDED(sattr3),
773 EXCLUSIVE([u8; 8]),
774}
775
776impl XdrEncode for createhow3 {
777 fn encode(&self, buf: &mut Vec<u8>) {
778 match self {
779 createhow3::UNCHECKED(a) => {
780 xdr_i32(buf, 0); a.encode(buf);
782 }
783 createhow3::GUARDED(a) => {
784 xdr_i32(buf, 1); a.encode(buf);
786 }
787 createhow3::EXCLUSIVE(v) => {
788 xdr_i32(buf, 2); buf.extend_from_slice(v);
790 }
791 }
792 }
793}
794
795#[derive(Debug, PartialEq)]
798pub(crate) struct NULL3args {}
799
800impl XdrEncode for NULL3args {
801 fn encode(&self, _buf: &mut Vec<u8>) {}
802}
803
804#[derive(Debug, PartialEq)]
805pub(crate) struct GETATTR3args {
806 pub(crate) object: nfs_fh3,
807}
808
809impl XdrEncode for GETATTR3args {
810 fn encode(&self, buf: &mut Vec<u8>) {
811 self.object.encode(buf);
812 }
813}
814
815#[derive(Debug, PartialEq)]
816pub(crate) struct ACCESS3args {
817 pub(crate) object: nfs_fh3,
818 pub(crate) access: u32,
819}
820
821impl XdrEncode for ACCESS3args {
822 fn encode(&self, buf: &mut Vec<u8>) {
823 self.object.encode(buf);
824 xdr_u32(buf, self.access);
825 }
826}
827
828#[derive(Debug, PartialEq)]
829pub(crate) struct READLINK3args {
830 pub(crate) symlink: nfs_fh3,
831}
832
833impl XdrEncode for READLINK3args {
834 fn encode(&self, buf: &mut Vec<u8>) {
835 self.symlink.encode(buf);
836 }
837}
838
839#[derive(Debug, PartialEq)]
840pub(crate) struct READ3args {
841 pub(crate) file: nfs_fh3,
842 pub(crate) offset: u64,
843 pub(crate) count: u32,
844}
845
846impl XdrEncode for READ3args {
847 fn encode(&self, buf: &mut Vec<u8>) {
848 self.file.encode(buf);
849 xdr_u64(buf, self.offset);
850 xdr_u32(buf, self.count);
851 }
852}
853
854#[derive(Debug, PartialEq)]
855pub(crate) struct WRITE3args {
856 pub(crate) file: nfs_fh3,
857 pub(crate) offset: u64,
858 pub(crate) count: u32,
859 pub(crate) stable: WriteStable,
860 pub(crate) data: Bytes,
861}
862
863#[allow(dead_code)]
864#[derive(Debug, PartialEq)]
865pub(crate) enum WriteStable {
866 Unstable = 0,
867 DataSync = 1,
868 FileSync = 2,
869}
870
871impl XdrEncode for WRITE3args {
872 fn encode(&self, buf: &mut Vec<u8>) {
873 self.file.encode(buf);
874 xdr_u64(buf, self.offset);
875 xdr_u32(buf, self.count);
876 xdr_i32(buf, self.stable.as_i32());
877 xdr_u32(buf, self.data.len() as u32);
880 }
881}
882
883impl WriteStable {
884 fn as_i32(&self) -> i32 {
885 match self {
886 WriteStable::Unstable => 0,
887 WriteStable::DataSync => 1,
888 WriteStable::FileSync => 2,
889 }
890 }
891}
892
893#[derive(Debug, PartialEq)]
894pub(crate) struct CREATE3args {
895 pub(crate) where_: diropargs3,
896 pub(crate) how: createhow3,
897}
898
899impl XdrEncode for CREATE3args {
900 fn encode(&self, buf: &mut Vec<u8>) {
901 self.where_.encode(buf);
902 self.how.encode(buf);
903 }
904}
905
906#[derive(Debug, PartialEq)]
907pub(crate) struct MKDIR3args {
908 pub(crate) where_: diropargs3,
909 pub(crate) attrs: sattr3,
910}
911
912impl XdrEncode for MKDIR3args {
913 fn encode(&self, buf: &mut Vec<u8>) {
914 self.where_.encode(buf);
915 self.attrs.encode(buf);
916 }
917}
918
919#[derive(Debug, PartialEq)]
920pub(crate) struct SYMLINK3args {
921 pub(crate) where_: diropargs3,
922 pub(crate) symlink: symlinkdata3,
923}
924
925impl XdrEncode for SYMLINK3args {
926 fn encode(&self, buf: &mut Vec<u8>) {
927 self.where_.encode(buf);
928 self.symlink.encode(buf);
929 }
930}
931
932#[derive(Debug, PartialEq)]
933pub(crate) struct REMOVE3args {
934 pub(crate) object: diropargs3,
935}
936
937impl XdrEncode for REMOVE3args {
938 fn encode(&self, buf: &mut Vec<u8>) {
939 self.object.encode(buf);
940 }
941}
942
943#[derive(Debug, PartialEq)]
944pub(crate) struct RMDIR3args {
945 pub(crate) object: diropargs3,
946}
947
948impl XdrEncode for RMDIR3args {
949 fn encode(&self, buf: &mut Vec<u8>) {
950 self.object.encode(buf);
951 }
952}
953
954#[derive(Debug, PartialEq)]
955pub(crate) struct RENAME3args {
956 pub(crate) from: diropargs3,
957 pub(crate) to: diropargs3,
958}
959
960impl XdrEncode for RENAME3args {
961 fn encode(&self, buf: &mut Vec<u8>) {
962 self.from.encode(buf);
963 self.to.encode(buf);
964 }
965}
966
967#[derive(Debug, PartialEq)]
968pub(crate) struct LINK3args {
969 pub(crate) file: nfs_fh3,
970 pub(crate) link: diropargs3,
971}
972
973impl XdrEncode for LINK3args {
974 fn encode(&self, buf: &mut Vec<u8>) {
975 self.file.encode(buf);
976 self.link.encode(buf);
977 }
978}
979
980#[derive(Debug, PartialEq)]
981pub(crate) struct READDIR3args {
982 pub(crate) dir: nfs_fh3,
983 pub(crate) cookie: u64,
984 pub(crate) cookieverf: [u8; 8],
985 pub(crate) count: u32,
986}
987
988impl XdrEncode for READDIR3args {
989 fn encode(&self, buf: &mut Vec<u8>) {
990 self.dir.encode(buf);
991 xdr_u64(buf, self.cookie);
992 buf.extend_from_slice(&self.cookieverf);
993 xdr_u32(buf, self.count);
994 }
995}
996
997#[derive(Debug, PartialEq)]
998pub(crate) struct READDIRPLUS3args {
999 pub(crate) dir: nfs_fh3,
1000 pub(crate) cookie: u64,
1001 pub(crate) cookieverf: [u8; 8],
1002 pub(crate) dircount: u32,
1003 pub(crate) maxcount: u32,
1004}
1005
1006impl XdrEncode for READDIRPLUS3args {
1007 fn encode(&self, buf: &mut Vec<u8>) {
1008 self.dir.encode(buf);
1009 xdr_u64(buf, self.cookie);
1010 buf.extend_from_slice(&self.cookieverf);
1011 xdr_u32(buf, self.dircount);
1012 xdr_u32(buf, self.maxcount);
1013 }
1014}
1015
1016#[derive(Debug, PartialEq)]
1017pub(crate) struct FSSTAT3args {
1018 pub(crate) fsroot: nfs_fh3,
1019}
1020
1021impl XdrEncode for FSSTAT3args {
1022 fn encode(&self, buf: &mut Vec<u8>) {
1023 self.fsroot.encode(buf);
1024 }
1025}
1026
1027#[derive(Debug, PartialEq)]
1028pub(crate) struct FSINFO3args {
1029 pub(crate) fsroot: nfs_fh3,
1030}
1031
1032impl XdrEncode for FSINFO3args {
1033 fn encode(&self, buf: &mut Vec<u8>) {
1034 self.fsroot.encode(buf);
1035 }
1036}
1037
1038#[derive(Debug, PartialEq)]
1039pub(crate) struct PATHCONF3args {
1040 pub(crate) object: nfs_fh3,
1041}
1042
1043impl XdrEncode for PATHCONF3args {
1044 fn encode(&self, buf: &mut Vec<u8>) {
1045 self.object.encode(buf);
1046 }
1047}
1048
1049#[derive(Debug, PartialEq)]
1050pub(crate) struct COMMIT3args {
1051 pub(crate) file: nfs_fh3,
1052 pub(crate) offset: u64,
1053 pub(crate) count: u32,
1054}
1055
1056impl XdrEncode for COMMIT3args {
1057 fn encode(&self, buf: &mut Vec<u8>) {
1058 self.file.encode(buf);
1059 xdr_u64(buf, self.offset);
1060 xdr_u32(buf, self.count);
1061 }
1062}
1063
1064#[derive(Debug, PartialEq)]
1065pub(crate) struct SETATTR3args {
1066 pub(crate) object: nfs_fh3,
1067 pub(crate) new_attributes: sattr3,
1068 pub(crate) guard: sattrguard3,
1069}
1070
1071impl XdrEncode for SETATTR3args {
1072 fn encode(&self, buf: &mut Vec<u8>) {
1073 self.object.encode(buf);
1074 self.new_attributes.encode(buf);
1075 self.guard.encode(buf);
1076 }
1077}
1078
1079#[derive(Debug, PartialEq)]
1080pub(crate) struct LOOKUP3args {
1081 pub(crate) what: diropargs3,
1082}
1083
1084impl XdrEncode for LOOKUP3args {
1085 fn encode(&self, buf: &mut Vec<u8>) {
1086 self.what.encode(buf);
1087 }
1088}
1089
1090pub(crate) fn encode_dirpath(buf: &mut Vec<u8>, path: &str) {
1094 xdr_string(buf, path);
1095}
1096
1097impl From<nfstime3> for Time {
1100 fn from(time: nfstime3) -> Self {
1101 Self {
1102 seconds: time.seconds,
1103 nseconds: time.nseconds,
1104 }
1105 }
1106}
1107
1108impl From<fattr3> for crate::mount::Attr {
1109 fn from(attr: fattr3) -> Self {
1110 Self {
1111 type_: attr.type_v as u32,
1112 file_mode: attr.mode.0,
1113 nlink: attr.nlink,
1114 uid: attr.uid.0,
1115 gid: attr.gid.0,
1116 filesize: attr.size.0,
1117 used: attr.used.0,
1118 spec_data: [attr.rdev.specdata1, attr.rdev.specdata2],
1119 fsid: attr.fsid,
1120 fileid: attr.fileid.0,
1121 atime: attr.atime.into(),
1122 mtime: attr.mtime.into(),
1123 ctime: attr.ctime.into(),
1124 acl: None,
1125 owner: String::new(),
1126 owner_group: String::new(),
1127 filehandle: Bytes::new(),
1128 }
1129 }
1130}
1131
1132impl From<post_op_attr> for Option<crate::mount::Attr> {
1133 fn from(attr: post_op_attr) -> Self {
1134 match attr {
1135 post_op_attr::TRUE(a) => Some(a.into()),
1136 post_op_attr::FALSE => None,
1137 }
1138 }
1139}
1140
1141impl From<FSINFO3resok> for crate::mount::FSInfo {
1142 fn from(ok: FSINFO3resok) -> Self {
1143 Self {
1144 attr: ok.obj_attributes.into(),
1145 rtmax: ok.rtmax,
1146 rtpref: ok.rtpref,
1147 rtmult: ok.rtmult,
1148 wtmax: ok.wtmax,
1149 wtpref: ok.wtpref,
1150 wtmult: ok.wtmult,
1151 dtpref: ok.dtpref,
1152 maxfilesize: ok.maxfilesize.0,
1153 time_delta: ok.time_delta.into(),
1154 properties: ok.properties,
1155 }
1156 }
1157}
1158
1159impl From<FSSTAT3resok> for crate::mount::FSStat {
1160 fn from(ok: FSSTAT3resok) -> Self {
1161 Self {
1162 attr: ok.obj_attributes.into(),
1163 tbytes: ok.tbytes.0,
1164 fbytes: ok.fbytes.0,
1165 abytes: ok.abytes.0,
1166 tfiles: ok.tfiles.0,
1167 ffiles: ok.ffiles.0,
1168 afiles: ok.afiles.0,
1169 invarsec: ok.invarsec,
1170 }
1171 }
1172}
1173
1174impl From<PATHCONF3resok> for crate::mount::Pathconf {
1175 fn from(ok: PATHCONF3resok) -> Self {
1176 Self {
1177 attr: ok.obj_attributes.into(),
1178 linkmax: ok.linkmax,
1179 name_max: ok.name_max,
1180 no_trunc: ok.no_trunc,
1181 chown_restricted: ok.chown_restricted,
1182 case_insensitive: ok.case_insensitive,
1183 case_preserving: ok.case_preserving,
1184 }
1185 }
1186}
1187
1188pub(crate) fn from_post_op_fh3(pofh: post_op_fh3) -> Result<Bytes> {
1190 match pofh {
1191 post_op_fh3::TRUE(fh) => Ok(fh.0),
1192 post_op_fh3::FALSE => Err(NfsError::Rpc("bad file handle".to_string())),
1193 }
1194}
1195
1196#[allow(unused)]
1197pub use fastxdr::nfsstat3 as ErrorCode;
1198
1199impl std::error::Error for ErrorCode {}
1200
1201impl std::fmt::Display for ErrorCode {
1202 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1203 match self {
1204 ErrorCode::NFS3_OK => write!(f, "call completed successfully"),
1205 ErrorCode::NFS3ERR_PERM => write!(f, "permission denied"),
1206 ErrorCode::NFS3ERR_NOENT => write!(f, "no such file or directory"),
1207 ErrorCode::NFS3ERR_NXIO => write!(f, "i/o error - no such device or address"),
1208 ErrorCode::NFS3ERR_ACCES => write!(f, "access denied"),
1209 ErrorCode::NFS3ERR_EXIST => write!(f, "file exists"),
1210 ErrorCode::NFS3ERR_XDEV => write!(f, "cross-device hard link not allowed"),
1211 ErrorCode::NFS3ERR_NODEV => write!(f, "no such device"),
1212 ErrorCode::NFS3ERR_NOTDIR => write!(f, "not a directory"),
1213 ErrorCode::NFS3ERR_ISDIR => write!(f, "is a directory"),
1214 ErrorCode::NFS3ERR_INVAL => write!(f, "invalid or unsupported argument"),
1215 ErrorCode::NFS3ERR_FBIG => write!(f, "file too large"),
1216 ErrorCode::NFS3ERR_NOSPC => write!(f, "no space left on device"),
1217 ErrorCode::NFS3ERR_ROFS => write!(f, "read-only file system"),
1218 ErrorCode::NFS3ERR_MLINK => write!(f, "too many hard links"),
1219 ErrorCode::NFS3ERR_NAMETOOLONG => write!(f, "name is too long"),
1220 ErrorCode::NFS3ERR_NOTEMPTY => write!(f, "directory not empty"),
1221 ErrorCode::NFS3ERR_DQUOT => write!(f, "resource (quota) hard limit exceeded"),
1222 ErrorCode::NFS3ERR_STALE => write!(f, "invalid file handle"),
1223 ErrorCode::NFS3ERR_REMOTE => write!(f, "too many levels of remote in path"),
1224 ErrorCode::NFS3ERR_BADHANDLE => write!(f, "illegal NFS file handle"),
1225 ErrorCode::NFS3ERR_NOT_SYNC => write!(f, "update synchronization mismatch"),
1226 ErrorCode::NFS3ERR_BAD_COOKIE => write!(f, "cookie is stale"),
1227 ErrorCode::NFS3ERR_NOTSUPP => write!(f, "operation is not supported"),
1228 ErrorCode::NFS3ERR_TOOSMALL => write!(f, "buffer or request is too small"),
1229 ErrorCode::NFS3ERR_SERVERFAULT => write!(f, "internal server error"),
1230 ErrorCode::NFS3ERR_BADTYPE => write!(f, "type not supported by server"),
1231 ErrorCode::NFS3ERR_JUKEBOX => write!(f, "try again"),
1232 ErrorCode::NFS3ERR_IO => write!(
1233 f,
1234 "i/o error occurred while processing the requested operation"
1235 ),
1236 }
1237 }
1238}
1239
1240#[allow(unused)]
1241pub use fastxdr::mount_mountstat3 as MountErrorCode;
1242
1243impl std::error::Error for MountErrorCode {}
1244
1245impl std::fmt::Display for MountErrorCode {
1246 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1247 match self {
1248 MountErrorCode::MNT3_OK => write!(f, "call completed successfully"),
1249 MountErrorCode::MNT3ERR_PERM => write!(f, "permission denied"),
1250 MountErrorCode::MNT3ERR_NOENT => write!(f, "no such file or directory"),
1251 MountErrorCode::MNT3ERR_ACCES => write!(f, "access denied"),
1252 MountErrorCode::MNT3ERR_NOTDIR => write!(f, "not a directory"),
1253 MountErrorCode::MNT3ERR_INVAL => write!(f, "invalid or unsupported argument"),
1254 MountErrorCode::MNT3ERR_NAMETOOLONG => write!(f, "name is too long"),
1255 MountErrorCode::MNT3ERR_NOTSUPP => write!(f, "operation is not supported"),
1256 MountErrorCode::MNT3ERR_SERVERFAULT => write!(f, "internal server error"),
1257 MountErrorCode::MNT3ERR_IO => write!(
1258 f,
1259 "i/o error occurred while processing the requested operation"
1260 ),
1261 }
1262 }
1263}
1264
1265#[cfg(test)]
1266mod tests {
1267 use super::*;
1268
1269 #[test]
1270 fn rpc_header_util() {
1271 let auth = crate::Auth::new_unix("machinist", 123, 987);
1272 let header = rpc_header(9, 8, 7, &auth);
1273 let expected = rpc::Header::new(rpc::RPC_VERSION, 9, 8, 7, &auth, &crate::Auth::new_null());
1274 assert_eq!(header, expected);
1275 }
1276
1277 #[test]
1280 fn xdr_u32_encodes_big_endian() {
1281 let mut buf = Vec::new();
1282 xdr_u32(&mut buf, 0x01020304);
1283 assert_eq!(buf, [0x01, 0x02, 0x03, 0x04]);
1284 }
1285
1286 #[test]
1287 fn xdr_u64_encodes_big_endian() {
1288 let mut buf = Vec::new();
1289 xdr_u64(&mut buf, 0x0102030405060708);
1290 assert_eq!(buf, [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]);
1291 }
1292
1293 #[test]
1294 fn xdr_i32_encodes_negative() {
1295 let mut buf = Vec::new();
1296 xdr_i32(&mut buf, -1);
1297 assert_eq!(buf, [0xFF, 0xFF, 0xFF, 0xFF]);
1298 }
1299
1300 #[test]
1301 fn xdr_fixed_bytes_no_padding() {
1302 let mut buf = Vec::new();
1303 xdr_fixed_bytes(&mut buf, &[1, 2, 3, 4]); assert_eq!(buf, [1, 2, 3, 4]);
1305 }
1306
1307 #[test]
1308 fn xdr_fixed_bytes_with_padding() {
1309 let mut buf = Vec::new();
1310 xdr_fixed_bytes(&mut buf, &[1, 2, 3]); assert_eq!(buf, [1, 2, 3, 0]);
1312
1313 let mut buf = Vec::new();
1314 xdr_fixed_bytes(&mut buf, &[1, 2]); assert_eq!(buf, [1, 2, 0, 0]);
1316
1317 let mut buf = Vec::new();
1318 xdr_fixed_bytes(&mut buf, &[1]); assert_eq!(buf, [1, 0, 0, 0]);
1320 }
1321
1322 #[test]
1323 fn xdr_fixed_bytes_empty() {
1324 let mut buf = Vec::new();
1325 xdr_fixed_bytes(&mut buf, &[]);
1326 assert!(buf.is_empty());
1327 }
1328
1329 #[test]
1330 fn xdr_var_bytes_encodes_length_prefix_and_padding() {
1331 let mut buf = Vec::new();
1332 xdr_var_bytes(&mut buf, b"hello"); assert_eq!(buf.len(), 12);
1334 assert_eq!(&buf[0..4], &5u32.to_be_bytes()); assert_eq!(&buf[4..9], b"hello");
1336 assert_eq!(&buf[9..12], &[0, 0, 0]); }
1338
1339 #[test]
1340 fn xdr_var_bytes_empty() {
1341 let mut buf = Vec::new();
1342 xdr_var_bytes(&mut buf, &[]);
1343 assert_eq!(buf, [0, 0, 0, 0]); }
1345
1346 #[test]
1347 fn xdr_string_encodes_as_var_bytes() {
1348 let mut buf1 = Vec::new();
1349 xdr_string(&mut buf1, "test");
1350 let mut buf2 = Vec::new();
1351 xdr_var_bytes(&mut buf2, b"test");
1352 assert_eq!(buf1, buf2);
1353 }
1354
1355 #[test]
1358 fn bytes_to_string_valid_utf8() {
1359 let b = Bytes::from("hello world");
1360 assert_eq!(bytes_to_string(b), "hello world");
1361 }
1362
1363 #[test]
1364 fn bytes_to_string_valid_utf8_unicode() {
1365 let b = Bytes::from("日本語テスト");
1366 assert_eq!(bytes_to_string(b), "日本語テスト");
1367 }
1368
1369 #[test]
1370 fn bytes_to_string_empty() {
1371 let b = Bytes::new();
1372 assert_eq!(bytes_to_string(b), "");
1373 }
1374
1375 #[test]
1376 fn bytes_to_string_invalid_utf8_lossy() {
1377 let b = Bytes::from_static(&[0xFF, 0xFE, 0x68, 0x69]); let result = bytes_to_string(b);
1379 assert!(result.contains("hi"));
1380 assert!(result.contains('\u{FFFD}')); }
1382
1383 #[test]
1386 fn hex_preview_short() {
1387 assert_eq!(hex_preview(&[0xAB, 0xCD, 0xEF]), "ab cd ef");
1388 }
1389
1390 #[test]
1391 fn hex_preview_truncates_at_32() {
1392 let data: Vec<u8> = (0..64).collect();
1393 let result = hex_preview(&data);
1394 assert!(result.ends_with("..."));
1395 assert!(!result.contains("20")); }
1398
1399 #[test]
1400 fn hex_preview_empty() {
1401 assert_eq!(hex_preview(&[]), "");
1402 }
1403}