nfs_rs/mount.rs
1// Copyright 2025 NetApp Inc. All Rights Reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14//
15// SPDX-License-Identifier: Apache-2.0
16
17use crate::Time;
18use crate::error::{NfsError, Result};
19use async_trait::async_trait;
20use bytes::Bytes;
21use futures::TryStreamExt;
22use futures::stream::Stream;
23use std::collections::HashSet;
24use std::pin::Pin;
25
26/// Tracks page boundaries without assuming cookies are ordered offsets.
27/// An empty non-EOF page or a repeated boundary must not masquerade as EOF.
28pub(crate) struct DirectoryCursor {
29 pub cookie: u64,
30 pub verifier: [u8; 8],
31 seen: HashSet<u64>,
32}
33
34impl Default for DirectoryCursor {
35 fn default() -> Self {
36 Self {
37 cookie: 0,
38 verifier: [0; 8],
39 seen: HashSet::from([0]),
40 }
41 }
42}
43
44impl DirectoryCursor {
45 pub fn advance(
46 &mut self,
47 cookie: u64,
48 verifier: [u8; 8],
49 entries: usize,
50 eof: bool,
51 ) -> Result<bool> {
52 if (!eof && entries == 0) || (entries > 0 && !self.seen.insert(cookie)) {
53 return Err(NfsError::Xdr(
54 "READDIR page made no progress or repeated a cookie".into(),
55 ));
56 }
57 self.cookie = cookie;
58 self.verifier = verifier;
59 Ok(!eof)
60 }
61}
62
63// Implementation payload ceiling; server and session limits may be smaller.
64pub(crate) const MAX_IO_SIZE: u32 = 4 * 1024 * 1024;
65
66pub(crate) fn negotiated_io_size(server_max: u64) -> Result<u32> {
67 let size = server_max.min(u64::from(MAX_IO_SIZE)) as u32;
68 if size == 0 {
69 return Err(NfsError::Xdr(
70 "server reported a zero maximum I/O size".into(),
71 ));
72 }
73 Ok(size)
74}
75
76/// An empty READ without EOF must not silently terminate a buffer fill.
77pub(crate) fn read_reply(data: Bytes, eof: bool) -> Result<Bytes> {
78 if data.is_empty() && !eof {
79 return Err(NfsError::Rpc("READ made no progress without EOF".into()));
80 }
81 Ok(data)
82}
83
84pub(crate) fn block_on_compat<F: std::future::Future>(f: F) -> F::Output {
85 match tokio::runtime::Handle::try_current() {
86 Ok(handle) if handle.runtime_flavor() == tokio::runtime::RuntimeFlavor::MultiThread => {
87 tokio::task::block_in_place(|| handle.block_on(f))
88 }
89 _ => futures::executor::block_on(f),
90 }
91}
92
93pub type ReaddirStream<'a> = Pin<Box<dyn Stream<Item = Result<ReaddirEntry>> + Send + 'a>>;
94
95pub type ReaddirplusStream<'a> = Pin<Box<dyn Stream<Item = Result<ReaddirplusEntry>> + Send + 'a>>;
96
97/// Access modes for [`Mount::open`].
98pub const OPEN_READ: u32 = 1;
99pub const OPEN_WRITE: u32 = 2;
100pub const OPEN_BOTH: u32 = 3;
101
102/// Server-reported WRITE commitment (RFC 1813 §3.3.7, RFC 7530 §16.38,
103/// RFC 5661 §18.32). This describes the response, not the requested stability.
104#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
105#[repr(u32)]
106pub enum WriteCommitted {
107 /// Data may be volatile; data and metadata still require synchronization.
108 Unstable = 0,
109 /// Data and metadata needed to recover it are durable; other metadata may not be.
110 DataSync = 1,
111 /// File data and metadata are durable.
112 FileSync = 2,
113}
114
115impl TryFrom<u32> for WriteCommitted {
116 type Error = NfsError;
117
118 fn try_from(value: u32) -> Result<Self> {
119 match value {
120 0 => Ok(Self::Unstable),
121 1 => Ok(Self::DataSync),
122 2 => Ok(Self::FileSync),
123 _ => Err(NfsError::Xdr(format!(
124 "invalid WRITE committed value: {value}"
125 ))),
126 }
127 }
128}
129
130/// Protocol write acknowledgement used by higher-level durability adapters.
131/// Result of one WRITE as reported by the server.
132#[derive(Clone, Debug)]
133pub struct WriteOutcome {
134 /// Bytes the server accepted (may be fewer than requested).
135 pub count: u32,
136 /// Actual server response. For pNFS, the lowest level across all DS
137 /// replies (including short-write retries) contributing to `count`.
138 /// Preserve the outcome for `commit_write_batch`, which also handles
139 /// required pNFS layout synchronization.
140 pub committed: WriteCommitted,
141 /// Single-server write verifier. pNFS verifiers are held in the opaque
142 /// receipt and checked by `commit_write_batch`. A changed verifier means
143 /// uncommitted data may have been lost and must be rewritten.
144 pub verifier: Option<[u8; 8]>,
145 /// Opaque routing information retained until batch commit completes.
146 pub(crate) pnfs: Option<std::sync::Arc<crate::nfs41::pnfs_io::PendingWrite>>,
147}
148
149impl WriteOutcome {
150 /// Construct an acknowledgement for a write to a single NFS server.
151 pub fn new(count: u32, committed: WriteCommitted, verifier: Option<[u8; 8]>) -> Self {
152 Self {
153 count,
154 committed,
155 verifier,
156 pnfs: None,
157 }
158 }
159}
160
161/// Stability level requested for a WRITE (`stable_how` in RFC 1813 §3.3.7,
162/// `stable_how4` in RFC 5661 §18.32). Internal: the public API exposes
163/// [`Mount::write`] (UNSTABLE).
164#[derive(Clone, Copy, Debug, Eq, PartialEq)]
165pub(crate) enum WriteStability {
166 /// The server may buffer the data; COMMIT before relying on it.
167 Unstable,
168}
169
170impl WriteStability {
171 /// Wire value shared by NFSv3 `stable_how` and NFSv4 `stable_how4`.
172 pub(crate) fn stable_how(self) -> u32 {
173 match self {
174 WriteStability::Unstable => 0,
175 }
176 }
177}
178
179/// Error for a write verifier that changed between WRITE and COMMIT: the
180/// server restarted and the uncommitted data must be rewritten.
181pub(crate) fn write_verifier_changed(protocol: NFSVersion) -> NfsError {
182 NfsError::OperationOutcome(Box::new(crate::error::OperationOutcomeError::new(
183 crate::error::OperationOutcome::Uncertain,
184 crate::error::OperationClass::ReplaySensitive,
185 crate::error::RecoveryAction::VerifyThenResume,
186 crate::error::RequestContext {
187 operation: "write_verifier".into(),
188 protocol,
189 request_id: None,
190 },
191 NfsError::Rpc("WRITE verifier changed before COMMIT".into()),
192 )))
193}
194
195/// Negotiated and currently effective NFSv4.1 fore-channel bounds.
196#[derive(Clone, Copy, Debug, Eq, PartialEq)]
197pub struct Nfs41ChannelLimits {
198 pub max_request_size: u32,
199 pub max_response_size: u32,
200 pub max_cached_response_size: u32,
201 pub max_operations: u32,
202 pub max_requests: u32,
203 pub effective_highest_slot_id: u32,
204}
205
206/// Redacted NFSv4.1 callback lifecycle counters for reliability diagnostics.
207#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
208pub struct Nfs41CallbackStats {
209 pub layout_recalls_received: u64,
210 pub layout_returns_completed: u64,
211}
212
213/// Protocol-neutral features exposed by a mounted client.
214#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
215pub struct MountCapabilities {
216 pub acl: bool,
217 pub named_attributes: bool,
218 pub locks: bool,
219 pub callbacks: bool,
220 pub delegation_retention: bool,
221 pub pnfs: bool,
222 pub session_diagnostics: bool,
223}
224
225/// High-level lifecycle state of a mount.
226#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
227pub enum MountLifecycleState {
228 #[default]
229 Ready,
230 Reconnecting,
231 Suspect,
232 Recovering,
233 Reclaiming,
234 LostState,
235 Closing,
236 Closed,
237}
238
239/// Protocol-neutral mount health. Stateful engines may override the defaults.
240#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
241pub struct MountHealth {
242 pub lifecycle: MountLifecycleState,
243 pub generation: u64,
244 pub lease_healthy: Option<bool>,
245 /// Server-advertised lease duration when the protocol exposes one.
246 pub lease_seconds: Option<u32>,
247 /// Number of validated background lease renewals in this generation.
248 pub lease_renewals: u64,
249 pub callback_healthy: Option<bool>,
250}
251
252/// Redacted callback counters shared by NFSv4 client engines.
253#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
254pub struct CallbackStats {
255 /// Delegations granted by the server and observed by the client.
256 pub grants_received: u64,
257 pub recalls_received: u64,
258 pub returns_completed: u64,
259 pub returns_failed: u64,
260}
261
262/// An opened object plus protocol-owned state hidden from callers.
263#[derive(Debug, PartialEq)]
264pub struct OpenFile {
265 pub object: ObjRes,
266 state: Option<Bytes>,
267}
268
269impl OpenFile {
270 pub(crate) fn from_object(object: ObjRes) -> Self {
271 Self {
272 object,
273 state: None,
274 }
275 }
276
277 pub(crate) fn with_protocol_state(object: ObjRes, state: Bytes) -> Self {
278 Self {
279 object,
280 state: Some(state),
281 }
282 }
283
284 pub(crate) fn into_parts(self) -> (ObjRes, Option<Bytes>) {
285 (self.object, self.state)
286 }
287
288 pub(crate) fn protocol_state(&self) -> Option<&Bytes> {
289 self.state.as_ref()
290 }
291
292 pub(crate) fn file_handle(&self) -> Bytes {
293 self.object.fh.clone()
294 }
295}
296
297/// A byte-range lock together with everything required to release it safely.
298#[derive(Debug, Eq, PartialEq)]
299pub struct LockToken {
300 pub(crate) fh: Bytes,
301 pub(crate) stateid: Bytes,
302 pub(crate) lock_type: u32,
303 pub(crate) offset: u64,
304 pub(crate) length: u64,
305 pub(crate) issuer: u64,
306 pub(crate) generation: u64,
307}
308
309impl LockToken {
310 pub(crate) fn new(
311 fh: Bytes,
312 stateid: Bytes,
313 lock_type: u32,
314 offset: u64,
315 length: u64,
316 issuer: u64,
317 generation: u64,
318 ) -> Self {
319 Self {
320 fh,
321 stateid,
322 lock_type,
323 offset,
324 length,
325 issuer,
326 generation,
327 }
328 }
329}
330
331/// Trait which defines the procedures that can be performed on an NFS mount.
332///
333/// NFS version agnostic. However, since NFSv4 introduces procedures that are not present in NFSv3, invoking those
334/// procedures will return an error when relevant [`Mount`] is NFSv3.
335///
336/// **Stale handles**: If the NFS server reboots, file handles become invalid and operations
337/// return `NfsError::Nfs3` with `NFS3ERR_STALE`. Callers should re-mount via
338/// [`parse_url_and_mount`](crate::parse_url_and_mount) to obtain fresh handles.
339#[async_trait]
340pub trait Mount: std::fmt::Debug + Send + Sync {
341 /// Return features implemented by this mounted client.
342 fn capabilities(&self) -> MountCapabilities {
343 MountCapabilities::default()
344 }
345
346 /// Return a redacted, protocol-neutral lifecycle snapshot.
347 fn health(&self) -> MountHealth {
348 MountHealth::default()
349 }
350
351 /// Return protocol-neutral callback counters.
352 async fn callback_stats(&self) -> CallbackStats {
353 let stats = self.nfs41_callback_stats().await.unwrap_or_default();
354 CallbackStats {
355 grants_received: 0,
356 recalls_received: stats.layout_recalls_received,
357 returns_completed: stats.layout_returns_completed,
358 returns_failed: 0,
359 }
360 }
361
362 /// Utility function get_max_read_size returns maximum read chunk size.
363 ///
364 /// # Example
365 ///
366 /// ```
367 /// async fn read_chunk(mount: &dyn nfs_rs::Mount, fh: bytes::Bytes, offset: u64, size: u32) -> nfs_rs::Result<bytes::Bytes> {
368 /// let chunk_size = mount.get_max_read_size().min(size);
369 /// mount.read(fh, offset, chunk_size).await
370 /// }
371 /// ```
372 fn get_max_read_size(&self) -> u32;
373
374 /// Utility function get_max_write_size returns maximum write chunk size.
375 ///
376 /// # Example
377 ///
378 /// ```
379 /// async fn write_chunk(mount: &dyn nfs_rs::Mount, fh: bytes::Bytes, offset: u64, data: &[u8], size: u32) -> nfs_rs::Result<u64> {
380 /// let chunk_size = mount.get_max_write_size().min(size) as usize;
381 /// let data = data[0..chunk_size].to_vec();
382 /// nfs_rs::write_all(mount, fh, offset, bytes::Bytes::from(data)).await
383 /// }
384 /// ```
385 fn get_max_write_size(&self) -> u32;
386
387 /// Return NFSv4.1 fore-channel limits, or `None` for other protocol versions.
388 async fn nfs41_channel_limits(&self) -> Option<Nfs41ChannelLimits> {
389 None
390 }
391
392 /// Return redacted callback counters, or `None` for other protocol versions.
393 async fn nfs41_callback_stats(&self) -> Option<Nfs41CallbackStats> {
394 None
395 }
396
397 /// Procedure NULL does not do any work. It is made available to allow server response testing and timing.
398 ///
399 /// # Example
400 ///
401 /// ```
402 /// async fn is_mount_alive(mount: &dyn nfs_rs::Mount) -> nfs_rs::Result<bool> {
403 /// mount.null().await?;
404 /// Ok(true)
405 /// }
406 /// ```
407 async fn null(&self) -> Result<()>;
408
409 /// Procedure ACCESS determines the access rights that a user, as identified by the credentials in the request,
410 /// has with respect to a file system object.
411 ///
412 /// # Example
413 ///
414 /// ```
415 /// async fn check_access(mount: &dyn nfs_rs::Mount, fh: bytes::Bytes, mode: u32) -> nfs_rs::Result<bool> {
416 /// let access = mount.access(fh, mode).await?;
417 /// Ok(mode == access)
418 /// }
419 /// ```
420 async fn access(&self, fh: Bytes, mode: u32) -> Result<u32>;
421
422 /// Same as [`Mount::access`] but instead of taking in a file handle, takes in a path for which file handle is
423 /// obtained by performing one or more LOOKUP procedures.
424 ///
425 /// # Example
426 ///
427 /// ```
428 /// async fn check_access_for_path(mount: &dyn nfs_rs::Mount, path: &str, mode: u32) -> nfs_rs::Result<bool> {
429 /// let access = mount.access_path(path, mode).await?;
430 /// Ok(mode == access)
431 /// }
432 /// ```
433 async fn access_path(&self, path: &str, mode: u32) -> Result<u32> {
434 let res = self.lookup_path(path).await?;
435 self.access(res.fh, mode).await
436 }
437
438 /// Open a file for read/write access, returning a file handle and attributes.
439 ///
440 /// For NFSv4.1 this sends OPEN and tracks the stateid internally.
441 /// For NFSv3 this is equivalent to [`Mount::lookup`] (stateless protocol).
442 ///
443 /// `access` is one of [`OPEN_READ`], [`OPEN_WRITE`], or [`OPEN_BOTH`].
444 ///
445 /// The returned [`ObjRes`] contains the file handle to use with
446 /// [`Mount::read`] / [`Mount::write`]. Call [`Mount::close`] when done.
447 async fn open(&self, dir_fh: Bytes, filename: &str, _access: u32) -> Result<ObjRes> {
448 // Default: stateless lookup (NFSv3)
449 self.lookup(dir_fh, filename).await
450 }
451
452 /// Same as [`Mount::open`] but takes a full path instead of dir_fh + filename.
453 async fn open_path(&self, path: &str, _access: u32) -> Result<ObjRes> {
454 // Default: stateless lookup (NFSv3)
455 self.lookup_path(path).await
456 }
457
458 /// Stateful form of [`Mount::open`]. Future protocol engines can attach
459 /// owner state without exposing wire stateids in the public API.
460 async fn open_stateful(&self, dir_fh: Bytes, filename: &str, access: u32) -> Result<OpenFile> {
461 Ok(OpenFile {
462 object: self.open(dir_fh, filename, access).await?,
463 state: None,
464 })
465 }
466
467 /// Stateful form of [`Mount::open_path`].
468 async fn open_path_stateful(&self, path: &str, access: u32) -> Result<OpenFile> {
469 Ok(OpenFile {
470 object: self.open_path(path, access).await?,
471 state: None,
472 })
473 }
474
475 /// Procedure CLOSE releases share reservations for a file (NFSv4).
476 /// For NFSv3 this is a no-op since NFSv3 is stateless.
477 /// The file handle should have been obtained via [`Mount::open`] or [`Mount::open_path`].
478 async fn close(&self, _fh: Bytes) -> Result<()> {
479 Ok(()) // Default: no-op for stateless protocols (NFSv3)
480 }
481
482 /// Close an object returned by a stateful open operation.
483 async fn close_stateful(&self, file: OpenFile) -> Result<()> {
484 let _protocol_state = file.state;
485 self.close(file.object.fh).await
486 }
487
488 /// Procedure COMMIT forces or flushes data to stable storage that was previously written with a WRITE procedure
489 /// call with the stable field set to UNSTABLE.
490 ///
491 /// # Example
492 ///
493 /// ```
494 /// async fn write_and_flush(mount: &dyn nfs_rs::Mount, fh: bytes::Bytes, offset: u64, data: &[u8]) -> nfs_rs::Result<()> {
495 /// let outcome = mount.write(fh.clone(), offset, bytes::Bytes::copy_from_slice(data)).await?;
496 /// mount.commit_write_batch(fh, offset, outcome.count, &[outcome]).await
497 /// }
498 /// ```
499 async fn commit(&self, fh: Bytes, offset: u64, count: u32) -> Result<()>;
500
501 #[doc(hidden)]
502 async fn commit_with_verifier(
503 &self,
504 fh: Bytes,
505 offset: u64,
506 count: u32,
507 ) -> Result<Option<[u8; 8]>> {
508 self.commit(fh, offset, count).await?;
509 Ok(None)
510 }
511
512 /// Complete a batch of WRITE acknowledgements, checking all verifiers.
513 /// pNFS implementations route COMMIT to the appropriate servers and
514 /// complete required LAYOUTCOMMITs. Retain the data until this succeeds.
515 /// The range must cover every acknowledgement; `(0, 0)` covers the file.
516 /// Pass only acknowledgements from writes on this mount and file handle.
517 async fn commit_write_batch(
518 &self,
519 fh: Bytes,
520 offset: u64,
521 count: u32,
522 writes: &[WriteOutcome],
523 ) -> Result<()> {
524 if writes
525 .iter()
526 .all(|w| w.committed == crate::WriteCommitted::FileSync)
527 {
528 return Ok(());
529 }
530 let actual = self.commit_with_verifier(fh, offset, count).await?;
531 verify_write_batch(self.version(), writes, actual)
532 }
533
534 /// Same as [`Mount::commit`] but instead of taking in a file handle, takes in a path for which file handle is
535 /// obtained by performing one or more LOOKUP procedures.
536 ///
537 /// # Example
538 ///
539 /// ```
540 /// async fn write_to_path_and_flush(mount: &dyn nfs_rs::Mount, path: &str, offset: u64, data: &[u8]) -> nfs_rs::Result<()> {
541 /// let obj = mount.lookup_path(path).await?;
542 /// nfs_rs::write_all(mount, obj.fh, offset, bytes::Bytes::copy_from_slice(data)).await?;
543 /// Ok(())
544 /// }
545 /// ```
546 async fn commit_path(&self, path: &str, offset: u64, count: u32) -> Result<()> {
547 let res = self.lookup_path(path).await?;
548 self.commit(res.fh, offset, count).await
549 }
550
551 /// Procedure CREATE creates a regular file.
552 ///
553 /// # Examples
554 ///
555 /// ```
556 /// async fn create_txt(mount: &dyn nfs_rs::Mount, dir_fh: bytes::Bytes, name: &str) -> nfs_rs::Result<nfs_rs::ObjRes> {
557 /// let mode = 0o640;
558 /// let filename = format!("{}.txt", name);
559 /// mount.create(dir_fh, &filename, Some(mode)).await
560 /// }
561 ///
562 /// async fn create_sh(mount: &dyn nfs_rs::Mount, dir_fh: bytes::Bytes, name: &str) -> nfs_rs::Result<nfs_rs::ObjRes> {
563 /// let mode = 0o750;
564 /// let filename = format!("{}.sh", name);
565 /// mount.create(dir_fh, &filename, Some(mode)).await
566 /// }
567 ///
568 /// async fn create_with_default_mode(mount: &dyn nfs_rs::Mount, dir_fh: bytes::Bytes, name: &str) -> nfs_rs::Result<nfs_rs::ObjRes> {
569 /// let filename = format!("{}.txt", name);
570 /// mount.create(dir_fh, &filename, None).await
571 /// }
572 /// ```
573 async fn create(&self, dir_fh: Bytes, filename: &str, mode: Option<u32>) -> Result<ObjRes>;
574
575 /// Same as [`Mount::create`] but instead of taking in directory file handle and filename, takes in a path for
576 /// which directory file handle is obtained by performing one or more LOOKUP procedures.
577 ///
578 /// # Examples
579 ///
580 /// ```
581 /// async fn create_txt(mount: &dyn nfs_rs::Mount, dir: &str, name: &str) -> nfs_rs::Result<nfs_rs::ObjRes> {
582 /// let mode = 0o640;
583 /// let path = format!("{}/{}.txt", dir, name);
584 /// mount.create_path(&path, Some(mode)).await
585 /// }
586 ///
587 /// async fn create_sh(mount: &dyn nfs_rs::Mount, dir: &str, name: &str) -> nfs_rs::Result<nfs_rs::ObjRes> {
588 /// let mode = 0o750;
589 /// let path = format!("{}/{}.sh", dir, name);
590 /// mount.create_path(&path, Some(mode)).await
591 /// }
592 ///
593 /// async fn create_path_with_default_mode(mount: &dyn nfs_rs::Mount, path: &str) -> nfs_rs::Result<nfs_rs::ObjRes> {
594 /// mount.create_path(path, None).await
595 /// }
596 /// ```
597 async fn create_path(&self, path: &str, mode: Option<u32>) -> Result<ObjRes>;
598
599 /// Stateful form of [`Mount::create_path`] used when the caller must later
600 /// release protocol-owned open state deterministically.
601 async fn create_path_stateful(&self, path: &str, mode: Option<u32>) -> Result<OpenFile> {
602 Ok(OpenFile::from_object(self.create_path(path, mode).await?))
603 }
604
605 /// Stateful create that preserves the caller's requested OPEN share access.
606 /// Stateless protocol engines may ignore the access after validating it.
607 async fn create_path_stateful_with_access(
608 &self,
609 path: &str,
610 mode: Option<u32>,
611 access: u32,
612 ) -> Result<OpenFile> {
613 if !matches!(access, OPEN_READ | OPEN_WRITE | OPEN_BOTH) {
614 return Err(NfsError::InvalidInput(format!(
615 "invalid create OPEN access {access}"
616 )));
617 }
618 self.create_path_stateful(path, mode).await
619 }
620
621 /// Procedure DELEGPURGE purges delegations (NFSv4 only; returns Unsupported on NFSv3).
622 #[deprecated(note = "delegation lifecycle is managed internally by the mount")]
623 async fn delegpurge(&self, _clientid: u64) -> Result<()> {
624 Err(NfsError::Unsupported(
625 "DELEGPURGE requires NFSv4".to_string(),
626 ))
627 }
628
629 /// Procedure DELEGRETURN returns a delegation (NFSv4 only; returns Unsupported on NFSv3).
630 #[deprecated(note = "delegation lifecycle is managed internally by the mount")]
631 async fn delegreturn(&self, _stateid: u64) -> Result<()> {
632 Err(NfsError::Unsupported(
633 "DELEGRETURN requires NFSv4".to_string(),
634 ))
635 }
636
637 /// Procedure LOCK acquires a byte-range lock (NFSv4.0/NFSv4.1 only; Unsupported on NFSv3).
638 /// lock_type: 1=READ, 2=WRITE. Returns the lock stateid on success.
639 async fn lock(&self, _fh: Bytes, _lock_type: u32, _offset: u64, _length: u64) -> Result<Bytes> {
640 Err(NfsError::Unsupported(
641 "LOCK requires NFSv4.0 or NFSv4.1".to_string(),
642 ))
643 }
644
645 /// Test whether a byte range can be locked without acquiring it (NFSv4.0/NFSv4.1 LOCKT).
646 async fn lock_test(
647 &self,
648 _fh: Bytes,
649 _lock_type: u32,
650 _offset: u64,
651 _length: u64,
652 ) -> Result<()> {
653 Err(NfsError::Unsupported(
654 "LOCKT requires NFSv4.0 or NFSv4.1".to_string(),
655 ))
656 }
657
658 /// Procedure LOCKU releases a byte-range lock (NFSv4 only; returns Unsupported on NFSv3).
659 async fn locku(
660 &self,
661 _fh: Bytes,
662 _lock_stateid: Bytes,
663 _lock_type: u32,
664 _offset: u64,
665 _length: u64,
666 ) -> Result<()> {
667 Err(NfsError::Unsupported("LOCKU requires NFSv4".to_string()))
668 }
669
670 /// Typed LOCK API that preserves the release parameters in one token.
671 async fn lock_stateful(
672 &self,
673 fh: Bytes,
674 lock_type: u32,
675 offset: u64,
676 length: u64,
677 ) -> Result<LockToken> {
678 let stateid = self.lock(fh.clone(), lock_type, offset, length).await?;
679 Ok(LockToken::new(fh, stateid, lock_type, offset, length, 0, 0))
680 }
681
682 /// Acquire a typed lock through a specific independent open-owner.
683 async fn lock_open_stateful(
684 &self,
685 opened: &OpenFile,
686 lock_type: u32,
687 offset: u64,
688 length: u64,
689 ) -> Result<LockToken> {
690 self.lock_stateful(opened.object.fh.clone(), lock_type, offset, length)
691 .await
692 }
693
694 /// Release a lock using the opaque token returned by [`Mount::lock_stateful`].
695 async fn unlock_stateful(&self, token: LockToken) -> Result<()> {
696 self.locku(
697 token.fh,
698 token.stateid,
699 token.lock_type,
700 token.offset,
701 token.length,
702 )
703 .await
704 }
705
706 /// Retrieve the NFSv4 ACL for a file (NFSv4 only; returns Unsupported on NFSv3).
707 async fn getacl(&self, _fh: Bytes) -> Result<Acl> {
708 Err(NfsError::Unsupported("GETACL requires NFSv4".to_string()))
709 }
710
711 /// Retrieve the NFSv4 ACL by path (NFSv4 only; returns Unsupported on NFSv3).
712 async fn getacl_path(&self, path: &str) -> Result<Acl> {
713 let res = self.lookup_path(path).await?;
714 self.getacl(res.fh).await
715 }
716
717 /// Set the NFSv4 ACL on a file (NFSv4 only; returns Unsupported on NFSv3).
718 async fn setacl(&self, _fh: Bytes, _acl: &Acl) -> Result<()> {
719 Err(NfsError::Unsupported("SETACL requires NFSv4".to_string()))
720 }
721
722 /// Set the NFSv4 ACL by path (NFSv4 only; returns Unsupported on NFSv3).
723 async fn setacl_path(&self, path: &str, acl: &Acl) -> Result<()> {
724 let res = self.lookup_path(path).await?;
725 self.setacl(res.fh, acl).await
726 }
727
728 /// Retrieve the NFSv4.1 discretionary ACL, including ACL-wide flags.
729 async fn getdacl(&self, _fh: Bytes) -> Result<NfsAcl41> {
730 Err(NfsError::Unsupported("DACL requires NFSv4.1".to_string()))
731 }
732
733 async fn getdacl_path(&self, path: &str) -> Result<NfsAcl41> {
734 let res = self.lookup_path(path).await?;
735 self.getdacl(res.fh).await
736 }
737
738 /// Atomically replace the complete NFSv4.1 discretionary ACL.
739 async fn setdacl(&self, _fh: Bytes, _acl: &NfsAcl41) -> Result<()> {
740 Err(NfsError::Unsupported("DACL requires NFSv4.1".to_string()))
741 }
742
743 async fn setdacl_path(&self, path: &str, acl: &NfsAcl41) -> Result<()> {
744 let res = self.lookup_path(path).await?;
745 self.setdacl(res.fh, acl).await
746 }
747
748 /// Retrieve the NFSv4.1 system ACL, including ACL-wide flags.
749 async fn getsacl(&self, _fh: Bytes) -> Result<NfsAcl41> {
750 Err(NfsError::Unsupported("SACL requires NFSv4.1".to_string()))
751 }
752
753 async fn getsacl_path(&self, path: &str) -> Result<NfsAcl41> {
754 let res = self.lookup_path(path).await?;
755 self.getsacl(res.fh).await
756 }
757
758 /// Atomically replace the complete NFSv4.1 system ACL.
759 async fn setsacl(&self, _fh: Bytes, _acl: &NfsAcl41) -> Result<()> {
760 Err(NfsError::Unsupported("SACL requires NFSv4.1".to_string()))
761 }
762
763 async fn setsacl_path(&self, path: &str, acl: &NfsAcl41) -> Result<()> {
764 let res = self.lookup_path(path).await?;
765 self.setsacl(res.fh, acl).await
766 }
767
768 /// Query which ACE types the server supports (FATTR4_ACLSUPPORT, NFSv4 only).
769 async fn aclsupport(&self, _fh: Bytes) -> Result<AclSupport> {
770 Err(NfsError::Unsupported(
771 "ACLSUPPORT requires NFSv4".to_string(),
772 ))
773 }
774
775 /// Get a named attribute (xattr) value (NFSv4 only; returns Unsupported on NFSv3).
776 /// NFSv4.1 reads to EOF, completing short reads, with a 1 MiB complete-value
777 /// ceiling. Oversized values return an error, never a truncated success.
778 async fn getxattr(&self, _fh: Bytes, _name: &str) -> Result<Bytes> {
779 Err(NfsError::Unsupported(
780 "Named attributes require NFSv4".to_string(),
781 ))
782 }
783
784 /// Get a named attribute (xattr) value by path.
785 async fn getxattr_path(&self, path: &str, name: &str) -> Result<Bytes> {
786 let res = self.lookup_path(path).await?;
787 self.getxattr(res.fh, name).await
788 }
789
790 /// Set a named attribute (xattr) value (NFSv4 only; returns Unsupported on NFSv3).
791 /// NFSv4.1 replaces the complete value (at most 1 MiB), including truncating
792 /// shorter/empty replacements. This is not atomic across clients; errors after
793 /// truncation carry an uncertain outcome and require verification.
794 async fn setxattr(&self, _fh: Bytes, _name: &str, _value: Bytes) -> Result<()> {
795 Err(NfsError::Unsupported(
796 "Named attributes require NFSv4".to_string(),
797 ))
798 }
799
800 /// Set a named attribute (xattr) value by path.
801 async fn setxattr_path(&self, path: &str, name: &str, value: Bytes) -> Result<()> {
802 let res = self.lookup_path(path).await?;
803 self.setxattr(res.fh, name, value).await
804 }
805
806 /// List named attribute (xattr) names (NFSv4 only; returns Unsupported on NFSv3).
807 async fn listxattr(&self, _fh: Bytes) -> Result<Vec<String>> {
808 Err(NfsError::Unsupported(
809 "Named attributes require NFSv4".to_string(),
810 ))
811 }
812
813 /// List named attribute (xattr) names by path.
814 async fn listxattr_path(&self, path: &str) -> Result<Vec<String>> {
815 let res = self.lookup_path(path).await?;
816 self.listxattr(res.fh).await
817 }
818
819 /// Remove a named attribute (xattr) (NFSv4 only; returns Unsupported on NFSv3).
820 async fn removexattr(&self, _fh: Bytes, _name: &str) -> Result<()> {
821 Err(NfsError::Unsupported(
822 "Named attributes require NFSv4".to_string(),
823 ))
824 }
825
826 /// Remove a named attribute (xattr) by path.
827 async fn removexattr_path(&self, path: &str, name: &str) -> Result<()> {
828 let res = self.lookup_path(path).await?;
829 self.removexattr(res.fh, name).await
830 }
831
832 /// Procedure FSINFO retrieves non-volatile file system state information.
833 ///
834 /// # Example
835 ///
836 /// ```
837 /// async fn get_max_filesize(mount: &dyn nfs_rs::Mount) -> nfs_rs::Result<u64> {
838 /// let info = mount.fsinfo().await?;
839 /// Ok(info.maxfilesize)
840 /// }
841 /// ```
842 async fn fsinfo(&self) -> Result<FSInfo>;
843
844 /// Procedure FSSTAT retrieves volatile file system state information.
845 ///
846 /// # Example
847 ///
848 /// ```
849 /// async fn has_space_for_files(mount: &dyn nfs_rs::Mount, num_files: u64, num_bytes: u64) -> nfs_rs::Result<bool> {
850 /// let fsstats = mount.fsstat().await?;
851 /// Ok(fsstats.ffiles >= num_files && fsstats.fbytes >= num_bytes)
852 /// }
853 /// ```
854 async fn fsstat(&self) -> Result<FSStat>;
855
856 /// Procedure GETATTR retrieves the attributes for a specified file system object.
857 ///
858 /// # Example
859 ///
860 /// ```
861 /// async fn has_changed_since(mount: &dyn nfs_rs::Mount, fh: bytes::Bytes, time: &nfs_rs::Time) -> nfs_rs::Result<bool> {
862 /// let attr = mount.getattr(fh).await?;
863 /// Ok(attr.mtime.seconds != time.seconds || attr.mtime.nseconds != time.nseconds)
864 /// }
865 /// ```
866 async fn getattr(&self, fh: Bytes) -> Result<Attr>;
867
868 /// Same as [`Mount::getattr`] but instead of taking in a file handle, takes in a path for which file handle is
869 /// obtained by performing one or more LOOKUP procedures.
870 ///
871 /// # Example
872 ///
873 /// ```
874 /// async fn has_path_changed_since(mount: &dyn nfs_rs::Mount, path: &str, time: &nfs_rs::Time) -> nfs_rs::Result<bool> {
875 /// let attr = mount.getattr_path(path).await?;
876 /// Ok(attr.mtime.seconds != time.seconds || attr.mtime.nseconds != time.nseconds)
877 /// }
878 /// ```
879 async fn getattr_path(&self, path: &str) -> Result<Attr> {
880 let res = self.lookup_path(path).await?;
881 self.getattr(res.fh).await
882 }
883
884 /// Procedure SETATTR changes one or more of the attributes of a file system object on the server.
885 ///
886 /// # Examples
887 ///
888 /// ```
889 /// async fn chmod(mount: &dyn nfs_rs::Mount, fh: bytes::Bytes, mode: u32, guard: Option<nfs_rs::Time>) -> nfs_rs::Result<()> {
890 /// mount.setattr(fh, guard, Some(mode), None, None, None, None, None).await
891 /// }
892 ///
893 /// async fn chown(mount: &dyn nfs_rs::Mount, fh: bytes::Bytes, uid: u32, gid: u32, guard: Option<nfs_rs::Time>) -> nfs_rs::Result<()> {
894 /// mount.setattr(fh, guard, None, Some(uid), Some(gid), None, None, None).await
895 /// }
896 ///
897 /// async fn truncate(mount: &dyn nfs_rs::Mount, fh: bytes::Bytes, size: u64, guard: Option<nfs_rs::Time>) -> nfs_rs::Result<()> {
898 /// mount.setattr(fh, guard, None, None, None, Some(size), None, None).await
899 /// }
900 ///
901 /// async fn touch(mount: &dyn nfs_rs::Mount, fh: bytes::Bytes, now: nfs_rs::Time, guard: Option<nfs_rs::Time>) -> nfs_rs::Result<()> {
902 /// let now_clone = now.clone();
903 /// mount.setattr(fh, guard, None, None, None, None, Some(now), Some(now_clone)).await
904 /// }
905 /// ```
906 #[allow(clippy::too_many_arguments)]
907 async fn setattr(
908 &self,
909 fh: Bytes,
910 guard_ctime: Option<Time>,
911 mode: Option<u32>,
912 uid: Option<u32>,
913 gid: Option<u32>,
914 size: Option<u64>,
915 atime: Option<Time>,
916 mtime: Option<Time>,
917 ) -> Result<()>;
918
919 /// Same as [`Mount::setattr`] but instead of taking in a file handle, takes in a path for which file handle is
920 /// obtained by performing one or more LOOKUP procedures. Also, instead of taking in optional guard ctime, takes
921 /// in a boolean which determines whether to specify guard in SETATTR procedure or not, using ctime from LOOKUP.
922 ///
923 /// # Examples
924 ///
925 /// ```
926 /// async fn chmod_path(mount: &dyn nfs_rs::Mount, path: &str, mode: u32, guard: bool) -> nfs_rs::Result<()> {
927 /// mount.setattr_path(path, guard, Some(mode), None, None, None, None, None).await
928 /// }
929 ///
930 /// async fn chown_path(mount: &dyn nfs_rs::Mount, path: &str, uid: u32, gid: u32, guard: bool) -> nfs_rs::Result<()> {
931 /// mount.setattr_path(path, guard, None, Some(uid), Some(gid), None, None, None).await
932 /// }
933 ///
934 /// async fn truncate_path(mount: &dyn nfs_rs::Mount, path: &str, size: u64, guard: bool) -> nfs_rs::Result<()> {
935 /// mount.setattr_path(path, guard, None, None, None, Some(size), None, None).await
936 /// }
937 ///
938 /// async fn touch_path(mount: &dyn nfs_rs::Mount, path: &str, now: nfs_rs::Time, guard: bool) -> nfs_rs::Result<()> {
939 /// let now_clone = now.clone();
940 /// mount.setattr_path(path, guard, None, None, None, None, Some(now), Some(now_clone)).await
941 /// }
942 /// ```
943 #[allow(clippy::too_many_arguments)]
944 async fn setattr_path(
945 &self,
946 path: &str,
947 specify_guard: bool,
948 mode: Option<u32>,
949 uid: Option<u32>,
950 gid: Option<u32>,
951 size: Option<u64>,
952 atime: Option<Time>,
953 mtime: Option<Time>,
954 ) -> Result<()>;
955
956 /// Procedure GETFH returns the current filehandle value.
957 async fn getfh(&self) -> Bytes;
958
959 /// Procedure LINK creates a hard link.
960 ///
961 /// # Example
962 ///
963 /// ```
964 /// async fn create_link(mount: &dyn nfs_rs::Mount, src_fh: bytes::Bytes, dst_dir_fh: bytes::Bytes, dst_filename: &str) -> nfs_rs::Result<nfs_rs::Attr> {
965 /// mount.link(src_fh, dst_dir_fh, dst_filename).await
966 /// }
967 /// ```
968 async fn link(&self, src_fh: Bytes, dst_dir_fh: Bytes, dst_filename: &str) -> Result<Attr>;
969
970 /// Same as [`Mount::link`] but instead of taking in a source file handle, destination directory file handle,
971 /// and destination filename, takes in a source path for which file handle is obtained by performing one or more
972 /// LOOKUP procedures and destination path for which directory file handle is obtained by performing one or more
973 /// LOOKUP procedures.
974 ///
975 /// # Example
976 ///
977 /// ```
978 /// async fn create_link_path(mount: &dyn nfs_rs::Mount, src_path: &str, dst_path: &str) -> nfs_rs::Result<nfs_rs::Attr> {
979 /// mount.link_path(src_path, dst_path).await
980 /// }
981 /// ```
982 async fn link_path(&self, src_path: &str, dst_path: &str) -> Result<Attr>;
983
984 /// Procedure SYMLINK creates a new symbolic link.
985 ///
986 /// # Example
987 ///
988 /// ```
989 /// async fn create_symlink(mount: &dyn nfs_rs::Mount, src_path: &str, dst_dir_fh: bytes::Bytes, dst_filename: &str) -> nfs_rs::Result<nfs_rs::ObjRes> {
990 /// mount.symlink(src_path, dst_dir_fh, dst_filename).await
991 /// }
992 /// ```
993 async fn symlink(
994 &self,
995 src_path: &str,
996 dst_dir_fh: Bytes,
997 dst_filename: &str,
998 ) -> Result<ObjRes>;
999
1000 /// Same as [`Mount::symlink`] but instead of taking in a destination directory file handle and destination
1001 /// filename, takes in a destination path for which directory file handle is obtained by performing one or more
1002 /// LOOKUP procedures.
1003 ///
1004 /// # Example
1005 ///
1006 /// ```
1007 /// async fn create_symlink(mount: &dyn nfs_rs::Mount, src_path: &str, dst_path: &str) -> nfs_rs::Result<nfs_rs::ObjRes> {
1008 /// mount.symlink_path(src_path, dst_path).await
1009 /// }
1010 /// ```
1011 async fn symlink_path(&self, src_path: &str, dst_path: &str) -> Result<ObjRes>;
1012
1013 /// Symbolic link creation with ownership / timestamps applied in the same
1014 /// network round trip when the backend supports it.
1015 ///
1016 /// Behaves like [`Mount::symlink`] when all attribute arguments are `None`.
1017 /// When any of `uid` / `gid` / `atime` / `mtime` is `Some`, implementations
1018 /// SHOULD bundle a SETATTR into the same NFSv4 COMPOUND, saving one RPC
1019 /// vs. the explicit CREATE-then-SETATTR pattern. The default implementation
1020 /// preserves the old behavior (two RPCs) so backends that cannot merge
1021 /// work without modification.
1022 ///
1023 /// On success, the symlink exists with the requested attributes applied.
1024 /// If the server rejects in-compound SETATTR, the implementation is
1025 /// expected to fall back to a separate SETATTR transparently.
1026 #[allow(clippy::too_many_arguments)]
1027 async fn symlink_with_attrs(
1028 &self,
1029 src_path: &str,
1030 dst_dir_fh: Bytes,
1031 dst_filename: &str,
1032 uid: Option<u32>,
1033 gid: Option<u32>,
1034 atime: Option<Time>,
1035 mtime: Option<Time>,
1036 ) -> Result<ObjRes> {
1037 let obj = self.symlink(src_path, dst_dir_fh, dst_filename).await?;
1038 if uid.is_some() || gid.is_some() || atime.is_some() || mtime.is_some() {
1039 self.setattr(obj.fh.clone(), None, None, uid, gid, None, atime, mtime)
1040 .await?;
1041 }
1042 Ok(obj)
1043 }
1044
1045 /// Procedure READLINK reads the data associated with a symbolic link.
1046 ///
1047 /// # Example
1048 ///
1049 /// ```
1050 /// async fn get_symlink_src_path(mount: &dyn nfs_rs::Mount, fh: bytes::Bytes) -> nfs_rs::Result<String> {
1051 /// mount.readlink(fh).await
1052 /// }
1053 /// ```
1054 async fn readlink(&self, fh: Bytes) -> Result<String>;
1055
1056 /// Same as [`Mount::readlink`] but instead of taking in a file handle, takes in a path for which file handle is
1057 /// obtained by performing one or more LOOKUP procedures.
1058 ///
1059 /// # Example
1060 ///
1061 /// ```
1062 /// async fn get_symlink_src_path_for_path(mount: &dyn nfs_rs::Mount, path: &str) -> nfs_rs::Result<String> {
1063 /// mount.readlink_path(path).await
1064 /// }
1065 /// ```
1066 async fn readlink_path(&self, path: &str) -> Result<String> {
1067 let res = self.lookup_path(path).await?;
1068 self.readlink(res.fh).await
1069 }
1070
1071 /// Procedure LOOKUP searches a directory for a specific name and returns the file handle and attributes for the
1072 /// corresponding file system object.
1073 ///
1074 /// # Example
1075 ///
1076 /// ```
1077 /// async fn is_directory(mount: &dyn nfs_rs::Mount, dir_fh: bytes::Bytes, filename: &str) -> nfs_rs::Result<bool> {
1078 /// const TYPE_DIR: u32 = 2;
1079 /// let res = mount.lookup(dir_fh, filename).await?;
1080 /// Ok(res.attr.map_or(false, |attr| attr.type_ == TYPE_DIR))
1081 /// }
1082 /// ```
1083 async fn lookup(&self, dir_fh: Bytes, filename: &str) -> Result<ObjRes>;
1084
1085 /// Same as [`Mount::lookup`] but instead of taking in a directory file handle and filename, takes in a path for
1086 /// which directory file handle is obtained by performing one or more LOOKUP procedures for each directory in the
1087 /// path, in turn.
1088 ///
1089 /// # Example
1090 ///
1091 /// ```
1092 /// async fn is_path_directory(mount: &dyn nfs_rs::Mount, path: &str) -> nfs_rs::Result<bool> {
1093 /// const TYPE_DIR: u32 = 2;
1094 /// let res = mount.lookup_path(path).await?;
1095 /// Ok(res.attr.map_or(false, |attr| attr.type_ == TYPE_DIR))
1096 /// }
1097 /// ```
1098 async fn lookup_path(&self, path: &str) -> Result<ObjRes>;
1099
1100 /// Procedure PATHCONF retrieves the pathconf information for a file or directory.
1101 ///
1102 /// # Example
1103 ///
1104 /// ```
1105 /// async fn chown_allowed(mount: &dyn nfs_rs::Mount, fh: bytes::Bytes) -> nfs_rs::Result<bool> {
1106 /// let conf = mount.pathconf(fh).await?;
1107 /// Ok(!conf.chown_restricted)
1108 /// }
1109 /// ```
1110 async fn pathconf(&self, fh: Bytes) -> Result<Pathconf>;
1111
1112 /// Returns path configuration together with the fields that were actually
1113 /// available. NFSv3 reports every field because PATHCONF3 has a fixed
1114 /// response shape; NFSv4 reports the GETATTR response bitmap and `fsid`.
1115 async fn pathconf_with_support(&self, fh: Bytes) -> Result<SupportedPathconf> {
1116 Ok(SupportedPathconf {
1117 values: self.pathconf(fh).await?,
1118 available: PathconfSupport::all(),
1119 fsid: None,
1120 })
1121 }
1122
1123 /// Same as [`Mount::pathconf`] but instead of taking in a file handle, takes in a path for which file handle is
1124 /// obtained by performing one or more LOOKUP procedures.
1125 ///
1126 /// # Example
1127 ///
1128 /// ```
1129 /// async fn chown_allowed_for_path(mount: &dyn nfs_rs::Mount, path: &str) -> nfs_rs::Result<bool> {
1130 /// let conf = mount.pathconf_path(path).await?;
1131 /// Ok(!conf.chown_restricted)
1132 /// }
1133 /// ```
1134 async fn pathconf_path(&self, path: &str) -> Result<Pathconf> {
1135 let res = self.lookup_path(path).await?;
1136 self.pathconf(res.fh).await
1137 }
1138
1139 /// Procedure READ reads data from a file.
1140 ///
1141 /// # Example
1142 ///
1143 /// ```
1144 /// async fn read_exact(mount: &dyn nfs_rs::Mount, fh: bytes::Bytes, mut offset: u64, mut count: u32) -> nfs_rs::Result<bytes::Bytes> {
1145 /// let mut ret = Vec::new();
1146 /// loop {
1147 /// let chunk = mount.read(fh.clone(), offset, count).await?;
1148 /// let chunk_len = chunk.len();
1149 /// count -= chunk_len as u32;
1150 /// ret.extend_from_slice(&chunk);
1151 /// if count == 0 {
1152 /// return Ok(bytes::Bytes::from(ret));
1153 /// }
1154 /// offset += chunk_len as u64;
1155 /// }
1156 /// }
1157 /// ```
1158 async fn read(&self, fh: Bytes, offset: u64, count: u32) -> Result<Bytes>;
1159
1160 /// Same as [`Mount::read`] but instead of taking in a file handle, takes in a path for which file handle is
1161 /// obtained by performing one or more LOOKUP procedures.
1162 ///
1163 /// # Example
1164 ///
1165 /// ```
1166 /// async fn read_exact_from_path(mount: &dyn nfs_rs::Mount, path: &str, mut offset: u64, mut count: u32) -> nfs_rs::Result<bytes::Bytes> {
1167 /// let mut ret = Vec::new();
1168 /// loop {
1169 /// let chunk = mount.read_path(path, offset, count).await?;
1170 /// let chunk_len = chunk.len();
1171 /// count -= chunk_len as u32;
1172 /// ret.extend_from_slice(&chunk);
1173 /// if count == 0 {
1174 /// return Ok(bytes::Bytes::from(ret));
1175 /// }
1176 /// offset += chunk_len as u64;
1177 /// }
1178 /// }
1179 /// ```
1180 async fn read_path(&self, path: &str, offset: u64, count: u32) -> Result<Bytes> {
1181 let res = self.lookup_path(path).await?;
1182 self.read(res.fh, offset, count).await
1183 }
1184
1185 /// Procedure WRITE with `stable = UNSTABLE`: the server may keep the
1186 /// data in volatile memory, so the caller must [`Mount::commit_write_batch`] before
1187 /// relying on it (RFC 1813 §3.3.7 / RFC 5661 §18.32). No COMMIT is issued
1188 /// here; the returned [`WriteOutcome`] says how many bytes the server
1189 /// took, the server's full [`WriteCommitted`] level, and the write verifier to
1190 /// pass to [`Mount::commit_write_batch`]. Use [`crate::write_all`] for a
1191 /// complete write that is durable when it returns.
1192 async fn write(&self, fh: Bytes, offset: u64, data: Bytes) -> Result<WriteOutcome>;
1193
1194 /// Same as [`Mount::write`] but takes a path resolved via LOOKUP.
1195 async fn write_path(&self, path: &str, offset: u64, data: Bytes) -> Result<WriteOutcome> {
1196 let res = self.lookup_path(path).await?;
1197 self.write(res.fh, offset, data).await
1198 }
1199
1200 /// Procedure READDIR retrieves a variable number of entries, in sequence, from a directory and returns the name
1201 /// and file identifier for each, with information to allow the client to request additional directory entries in
1202 /// a subsequent READDIR request.
1203 ///
1204 /// # Example
1205 ///
1206 /// ```
1207 /// use futures::TryStreamExt;
1208 /// async fn list_entries(mount: &dyn nfs_rs::Mount, dir_fh: bytes::Bytes) -> nfs_rs::Result<()> {
1209 /// let mut stream = mount.readdir(dir_fh).await;
1210 /// while let Some(entry) = stream.try_next().await? {
1211 /// println!("{}", entry.file_name);
1212 /// }
1213 /// Ok(())
1214 /// }
1215 /// ```
1216 async fn readdir(&self, dir_fh: Bytes) -> ReaddirStream<'_>;
1217
1218 /// Same as [`Mount::readdir`] but instead of taking in a directory file handle, takes in a path for which
1219 /// directory file handle is obtained by performing one or more LOOKUP procedures.
1220 ///
1221 /// # Example
1222 ///
1223 /// ```
1224 /// use futures::TryStreamExt;
1225 /// async fn list_entries_in_path(mount: &dyn nfs_rs::Mount, dir_path: &str) -> nfs_rs::Result<()> {
1226 /// let mut stream = mount.readdir_path(dir_path).await?;
1227 /// while let Some(entry) = stream.try_next().await? {
1228 /// println!("{}", entry.file_name);
1229 /// }
1230 /// Ok(())
1231 /// }
1232 /// ```
1233 async fn readdir_path(&self, dir_path: &str) -> Result<ReaddirStream<'_>> {
1234 let res = self.lookup_path(dir_path).await?;
1235 Ok(self.readdir(res.fh).await)
1236 }
1237
1238 /// Procedure READDIRPLUS retrieves a variable number of entries from a file system directory and returns complete
1239 /// information about each along with information to allow the client to request additional directory entries in a
1240 /// subsequent READDIRPLUS. READDIRPLUS differs from READDIR only in the amount of information returned for each
1241 /// entry. In READDIR, each entry returns the filename and the fileid. In READDIRPLUS, each entry returns the
1242 /// name, the fileid, attributes (including the fileid), and file handle.
1243 ///
1244 /// Returns a [`Stream`] of [`ReaddirplusEntry`] items, yielding entries lazily page by page.
1245 ///
1246 /// # Example
1247 ///
1248 /// ```
1249 /// use futures::TryStreamExt;
1250 /// async fn list_detailed_entries(mount: &dyn nfs_rs::Mount, dir_fh: bytes::Bytes) -> nfs_rs::Result<()> {
1251 /// println!("{:>5} {:>25} {}", "mode", "size", "name");
1252 /// let mut stream = mount.readdirplus(dir_fh).await;
1253 /// while let Some(entry) = stream.try_next().await? {
1254 /// let (mode, size) = entry.attr.map_or((String::new(), String::new()), |attr| {
1255 /// (
1256 /// format!("{:o}", attr.file_mode),
1257 /// format!("{}", attr.filesize),
1258 /// )
1259 /// });
1260 /// println!("{:>5} {:>25} {}", mode, size, entry.file_name);
1261 /// }
1262 /// Ok(())
1263 /// }
1264 /// ```
1265 async fn readdirplus(&self, dir_fh: Bytes) -> ReaddirplusStream<'_>;
1266
1267 /// Same as [`Mount::readdirplus`] but instead of taking in a directory file handle, takes in a path for which
1268 /// directory file handle is obtained by performing one or more LOOKUP procedures.
1269 ///
1270 /// # Example
1271 ///
1272 /// ```
1273 /// use futures::TryStreamExt;
1274 /// async fn list_detailed_entries_in_path(mount: &dyn nfs_rs::Mount, dir_path: &str) -> nfs_rs::Result<()> {
1275 /// println!("{:>5} {:>25} {}", "mode", "size", "name");
1276 /// let mut stream = mount.readdirplus_path(dir_path).await?;
1277 /// while let Some(entry) = stream.try_next().await? {
1278 /// let (mode, size) = entry.attr.map_or((String::new(), String::new()), |attr| {
1279 /// (
1280 /// format!("{:o}", attr.file_mode),
1281 /// format!("{}", attr.filesize),
1282 /// )
1283 /// });
1284 /// println!("{:>5} {:>25} {}", mode, size, entry.file_name);
1285 /// }
1286 /// Ok(())
1287 /// }
1288 /// ```
1289 async fn readdirplus_path(&self, dir_path: &str) -> Result<ReaddirplusStream<'_>> {
1290 let res = self.lookup_path(dir_path).await?;
1291 Ok(self.readdirplus(res.fh).await)
1292 }
1293
1294 /// Procedure MKDIR creates a new subdirectory.
1295 ///
1296 /// # Example
1297 ///
1298 /// ```
1299 /// async fn ensure_directory_exists(mount: &dyn nfs_rs::Mount, dir_fh: bytes::Bytes, dirname: &str) -> nfs_rs::Result<()> {
1300 /// let mode = 0o750;
1301 /// let res = mount.mkdir(dir_fh, dirname, mode).await;
1302 /// if res.is_err() {
1303 /// let err = res.unwrap_err();
1304 /// if err.to_string() != "file exists" {
1305 /// return Err(err);
1306 /// }
1307 /// }
1308 /// Ok(())
1309 /// }
1310 /// ```
1311 async fn mkdir(&self, dir_fh: Bytes, dirname: &str, mode: u32) -> Result<ObjRes>;
1312
1313 /// Same as [`Mount::mkdir`] but instead of taking in directory file handle and dirname, takes in a path for which
1314 /// directory file handle is obtained by performing one or more LOOKUP procedures.
1315 ///
1316 /// # Example
1317 ///
1318 /// ```
1319 /// async fn ensure_directory_exists_for_path(mount: &dyn nfs_rs::Mount, path: &str) -> nfs_rs::Result<()> {
1320 /// let mode = 0o750;
1321 /// let res = mount.mkdir_path(path, mode).await;
1322 /// if res.is_err() {
1323 /// let err = res.unwrap_err();
1324 /// if err.to_string() != "file exists" {
1325 /// return Err(err);
1326 /// }
1327 /// }
1328 /// Ok(())
1329 /// }
1330 /// ```
1331 async fn mkdir_path(&self, path: &str, mode: u32) -> Result<ObjRes>;
1332
1333 /// Procedure REMOVE removes (deletes) an entry from a directory.
1334 ///
1335 /// # Example
1336 ///
1337 /// ```
1338 /// async fn delete_file(mount: &dyn nfs_rs::Mount, dir_fh: bytes::Bytes, filename: &str) -> nfs_rs::Result<()> {
1339 /// mount.remove(dir_fh, filename).await
1340 /// }
1341 /// ```
1342 async fn remove(&self, dir_fh: Bytes, filename: &str) -> Result<()>;
1343
1344 /// Same as [`Mount::remove`] but instead of taking in a directory file handle and filename, takes in a path for
1345 /// which directory file handle is obtained by performing one or more LOOKUP procedures.
1346 ///
1347 /// # Example
1348 ///
1349 /// ```
1350 /// async fn delete_file_path(mount: &dyn nfs_rs::Mount, path: &str) -> nfs_rs::Result<()> {
1351 /// mount.remove_path(path).await
1352 /// }
1353 /// ```
1354 async fn remove_path(&self, path: &str) -> Result<()>;
1355
1356 /// Procedure RMDIR removes (deletes) a subdirectory from a directory.
1357 ///
1358 /// # Example
1359 ///
1360 /// ```
1361 /// async fn delete_directory(mount: &dyn nfs_rs::Mount, dir_fh: bytes::Bytes, dirname: &str) -> nfs_rs::Result<()> {
1362 /// mount.rmdir(dir_fh, dirname).await
1363 /// }
1364 /// ```
1365 async fn rmdir(&self, dir_fh: Bytes, dirname: &str) -> Result<()>;
1366
1367 /// Same as [`Mount::rmdir`] but instead of taking in a directory file handle and directory name, takes in a path
1368 /// for which directory file handle is obtained by performing one or more LOOKUP procedures.
1369 ///
1370 /// # Example
1371 ///
1372 /// ```
1373 /// async fn delete_directory_path(mount: &dyn nfs_rs::Mount, path: &str) -> nfs_rs::Result<()> {
1374 /// mount.rmdir_path(path).await
1375 /// }
1376 /// ```
1377 async fn rmdir_path(&self, path: &str) -> Result<()>;
1378
1379 // Procedure RENAME renames an entry.
1380 ///
1381 /// # Example
1382 ///
1383 /// ```
1384 /// async fn mv(
1385 /// mount: &dyn nfs_rs::Mount,
1386 /// from_dir_fh: bytes::Bytes,
1387 /// from_filename: &str,
1388 /// to_dir_fh: bytes::Bytes,
1389 /// to_filename: &str,
1390 /// ) -> nfs_rs::Result<()> {
1391 /// mount.rename(from_dir_fh, from_filename, to_dir_fh, to_filename).await
1392 /// }
1393 /// ```
1394 async fn rename(
1395 &self,
1396 from_dir_fh: Bytes,
1397 from_filename: &str,
1398 to_dir_fh: Bytes,
1399 to_filename: &str,
1400 ) -> Result<()>;
1401
1402 /// Same as [`Mount::rename`] but instead of taking in a from directory file handle, from filename, to directory
1403 /// file handle, and to filename, takes in a from path for which directory file handle is obtained by performing
1404 /// one or more LOOKUP procedures and to path for which directory file handle is obtained by performing one or more
1405 /// LOOKUP procedures.
1406 ///
1407 /// # Example
1408 ///
1409 /// ```
1410 /// async fn mv_path(mount: &dyn nfs_rs::Mount, from_path: &str, to_path: &str) -> nfs_rs::Result<()> {
1411 /// mount.rename_path(from_path, to_path).await
1412 /// }
1413 /// ```
1414 async fn rename_path(&self, from_path: &str, to_path: &str) -> Result<()>;
1415
1416 /// Procedure UMOUNT unmounts the mount itself.
1417 ///
1418 /// # Example
1419 ///
1420 /// ```
1421 /// async fn unmount(mount: &dyn nfs_rs::Mount) -> nfs_rs::Result<()> {
1422 /// mount.umount().await
1423 /// }
1424 /// ```
1425 async fn umount(&self) -> Result<()>;
1426
1427 /// Return NFS version
1428 ///
1429 /// # Example
1430 ///
1431 /// ```
1432 /// fn list_unsupported_procedures(mount: &dyn nfs_rs::Mount) -> Option<Vec<String>> {
1433 /// match mount.version() {
1434 /// nfs_rs::NFSVersion::NFSv3 => Some(vec![
1435 /// String::from("close"),
1436 /// String::from("delegpurge"),
1437 /// String::from("delegreturn"),
1438 /// String::from("getfh"),
1439 /// ]),
1440 /// _ => None,
1441 /// }
1442 /// }
1443 /// ```
1444 fn version(&self) -> NFSVersion;
1445
1446 /// Blocking version of [`Mount::null`]
1447 fn sync_null(&self) -> Result<()> {
1448 block_on_compat(self.null())
1449 }
1450
1451 /// Blocking version of [`Mount::access`]
1452 fn sync_access(&self, fh: Bytes, mode: u32) -> Result<u32> {
1453 block_on_compat(self.access(fh, mode))
1454 }
1455
1456 /// Blocking version of [`Mount::access_path`]
1457 fn sync_access_path(&self, path: &str, mode: u32) -> Result<u32> {
1458 block_on_compat(self.access_path(path, mode))
1459 }
1460
1461 /// Blocking version of [`Mount::open`]
1462 fn sync_open(&self, dir_fh: Bytes, filename: &str, access: u32) -> Result<ObjRes> {
1463 block_on_compat(self.open(dir_fh, filename, access))
1464 }
1465
1466 /// Blocking version of [`Mount::open_path`]
1467 fn sync_open_path(&self, path: &str, access: u32) -> Result<ObjRes> {
1468 block_on_compat(self.open_path(path, access))
1469 }
1470
1471 /// Blocking version of [`Mount::close`]
1472 fn sync_close(&self, fh: Bytes) -> Result<()> {
1473 block_on_compat(self.close(fh))
1474 }
1475
1476 /// Blocking version of [`Mount::commit`]
1477 fn sync_commit(&self, fh: Bytes, offset: u64, count: u32) -> Result<()> {
1478 block_on_compat(self.commit(fh, offset, count))
1479 }
1480
1481 /// Blocking version of [`Mount::commit_path`]
1482 fn sync_commit_path(&self, path: &str, offset: u64, count: u32) -> Result<()> {
1483 block_on_compat(self.commit_path(path, offset, count))
1484 }
1485
1486 /// Blocking version of [`Mount::create`]
1487 fn sync_create(&self, dir_fh: Bytes, filename: &str, mode: Option<u32>) -> Result<ObjRes> {
1488 block_on_compat(self.create(dir_fh, filename, mode))
1489 }
1490
1491 /// Blocking version of [`Mount::create_path`]
1492 fn sync_create_path(&self, path: &str, mode: Option<u32>) -> Result<ObjRes> {
1493 block_on_compat(self.create_path(path, mode))
1494 }
1495
1496 /// Blocking version of [`Mount::delegpurge`]
1497 #[allow(deprecated)]
1498 fn sync_delegpurge(&self, clientid: u64) -> Result<()> {
1499 block_on_compat(self.delegpurge(clientid))
1500 }
1501
1502 /// Blocking version of [`Mount::delegreturn`]
1503 #[allow(deprecated)]
1504 fn sync_delegreturn(&self, stateid: u64) -> Result<()> {
1505 block_on_compat(self.delegreturn(stateid))
1506 }
1507
1508 /// Blocking version of [`Mount::fsinfo`]
1509 fn sync_fsinfo(&self) -> Result<FSInfo> {
1510 block_on_compat(self.fsinfo())
1511 }
1512
1513 /// Blocking version of [`Mount::fsstat`]
1514 fn sync_fsstat(&self) -> Result<FSStat> {
1515 block_on_compat(self.fsstat())
1516 }
1517
1518 /// Blocking version of [`Mount::getattr`]
1519 fn sync_getattr(&self, fh: Bytes) -> Result<Attr> {
1520 block_on_compat(self.getattr(fh))
1521 }
1522
1523 /// Blocking version of [`Mount::getattr_path`]
1524 fn sync_getattr_path(&self, path: &str) -> Result<Attr> {
1525 block_on_compat(self.getattr_path(path))
1526 }
1527
1528 /// Blocking version of [`Mount::setattr`]
1529 #[allow(clippy::too_many_arguments)]
1530 fn sync_setattr(
1531 &self,
1532 fh: Bytes,
1533 guard_ctime: Option<Time>,
1534 mode: Option<u32>,
1535 uid: Option<u32>,
1536 gid: Option<u32>,
1537 size: Option<u64>,
1538 atime: Option<Time>,
1539 mtime: Option<Time>,
1540 ) -> Result<()> {
1541 block_on_compat(self.setattr(fh, guard_ctime, mode, uid, gid, size, atime, mtime))
1542 }
1543
1544 /// Blocking version of [`Mount::setattr_path`]
1545 #[allow(clippy::too_many_arguments)]
1546 fn sync_setattr_path(
1547 &self,
1548 path: &str,
1549 specify_guard: bool,
1550 mode: Option<u32>,
1551 uid: Option<u32>,
1552 gid: Option<u32>,
1553 size: Option<u64>,
1554 atime: Option<Time>,
1555 mtime: Option<Time>,
1556 ) -> Result<()> {
1557 block_on_compat(self.setattr_path(path, specify_guard, mode, uid, gid, size, atime, mtime))
1558 }
1559
1560 /// Blocking version of [`Mount::getfh`]
1561 fn sync_getfh(&self) -> Bytes {
1562 block_on_compat(self.getfh())
1563 }
1564
1565 /// Blocking version of [`Mount::link`]
1566 fn sync_link(&self, src_fh: Bytes, dst_dir_fh: Bytes, dst_filename: &str) -> Result<Attr> {
1567 block_on_compat(self.link(src_fh, dst_dir_fh, dst_filename))
1568 }
1569
1570 /// Blocking version of [`Mount::link_path`]
1571 fn sync_link_path(&self, src_path: &str, dst_path: &str) -> Result<Attr> {
1572 block_on_compat(self.link_path(src_path, dst_path))
1573 }
1574
1575 /// Blocking version of [`Mount::symlink`]
1576 fn sync_symlink(
1577 &self,
1578 src_path: &str,
1579 dst_dir_fh: Bytes,
1580 dst_filename: &str,
1581 ) -> Result<ObjRes> {
1582 block_on_compat(self.symlink(src_path, dst_dir_fh, dst_filename))
1583 }
1584
1585 /// Blocking version of [`Mount::symlink_path`]
1586 fn sync_symlink_path(&self, src_path: &str, dst_path: &str) -> Result<ObjRes> {
1587 block_on_compat(self.symlink_path(src_path, dst_path))
1588 }
1589
1590 /// Blocking version of [`Mount::readlink`]
1591 fn sync_readlink(&self, fh: Bytes) -> Result<String> {
1592 block_on_compat(self.readlink(fh))
1593 }
1594
1595 /// Blocking version of [`Mount::readlink_path`]
1596 fn sync_readlink_path(&self, path: &str) -> Result<String> {
1597 block_on_compat(self.readlink_path(path))
1598 }
1599
1600 /// Blocking version of [`Mount::lookup`]
1601 fn sync_lookup(&self, dir_fh: Bytes, filename: &str) -> Result<ObjRes> {
1602 block_on_compat(self.lookup(dir_fh, filename))
1603 }
1604
1605 /// Blocking version of [`Mount::lookup_path`]
1606 fn sync_lookup_path(&self, path: &str) -> Result<ObjRes> {
1607 block_on_compat(self.lookup_path(path))
1608 }
1609
1610 /// Blocking version of [`Mount::pathconf`]
1611 fn sync_pathconf(&self, fh: Bytes) -> Result<Pathconf> {
1612 block_on_compat(self.pathconf(fh))
1613 }
1614
1615 /// Blocking version of [`Mount::pathconf_path`]
1616 fn sync_pathconf_path(&self, path: &str) -> Result<Pathconf> {
1617 block_on_compat(self.pathconf_path(path))
1618 }
1619
1620 /// Blocking version of [`Mount::read`]
1621 fn sync_read(&self, fh: Bytes, offset: u64, count: u32) -> Result<Bytes> {
1622 block_on_compat(self.read(fh, offset, count))
1623 }
1624
1625 /// Blocking version of [`Mount::read_path`]
1626 fn sync_read_path(&self, path: &str, offset: u64, count: u32) -> Result<Bytes> {
1627 block_on_compat(self.read_path(path, offset, count))
1628 }
1629
1630 /// Blocking version of [`Mount::write`]
1631 fn sync_write(&self, fh: Bytes, offset: u64, data: Bytes) -> Result<WriteOutcome> {
1632 block_on_compat(self.write(fh, offset, data))
1633 }
1634
1635 /// Blocking version of [`Mount::write_path`]
1636 fn sync_write_path(&self, path: &str, offset: u64, data: Bytes) -> Result<WriteOutcome> {
1637 block_on_compat(self.write_path(path, offset, data))
1638 }
1639
1640 /// Blocking version of [`Mount::readdir`]
1641 fn sync_readdir(&self, dir_fh: Bytes) -> Result<Vec<ReaddirEntry>> {
1642 block_on_compat(async { self.readdir(dir_fh).await.try_collect().await })
1643 }
1644
1645 /// Blocking version of [`Mount::readdir_path`]
1646 fn sync_readdir_path(&self, dir_path: &str) -> Result<Vec<ReaddirEntry>> {
1647 block_on_compat(async { self.readdir_path(dir_path).await?.try_collect().await })
1648 }
1649
1650 /// Blocking version of [`Mount::readdirplus`]
1651 fn sync_readdirplus(&self, dir_fh: Bytes) -> Result<Vec<ReaddirplusEntry>> {
1652 block_on_compat(async { self.readdirplus(dir_fh).await.try_collect().await })
1653 }
1654
1655 /// Blocking version of [`Mount::readdirplus_path`]
1656 fn sync_readdirplus_path(&self, dir_path: &str) -> Result<Vec<ReaddirplusEntry>> {
1657 block_on_compat(async { self.readdirplus_path(dir_path).await?.try_collect().await })
1658 }
1659
1660 /// Blocking version of [`Mount::mkdir`]
1661 fn sync_mkdir(&self, dir_fh: Bytes, dirname: &str, mode: u32) -> Result<ObjRes> {
1662 block_on_compat(self.mkdir(dir_fh, dirname, mode))
1663 }
1664
1665 /// Blocking version of [`Mount::mkdir_path`]
1666 fn sync_mkdir_path(&self, path: &str, mode: u32) -> Result<ObjRes> {
1667 block_on_compat(self.mkdir_path(path, mode))
1668 }
1669
1670 /// Blocking version of [`Mount::remove`]
1671 fn sync_remove(&self, dir_fh: Bytes, filename: &str) -> Result<()> {
1672 block_on_compat(self.remove(dir_fh, filename))
1673 }
1674
1675 /// Blocking version of [`Mount::remove_path`]
1676 fn sync_remove_path(&self, path: &str) -> Result<()> {
1677 block_on_compat(self.remove_path(path))
1678 }
1679
1680 /// Blocking version of [`Mount::rmdir`]
1681 fn sync_rmdir(&self, dir_fh: Bytes, dirname: &str) -> Result<()> {
1682 block_on_compat(self.rmdir(dir_fh, dirname))
1683 }
1684
1685 /// Blocking version of [`Mount::rmdir_path`]
1686 fn sync_rmdir_path(&self, path: &str) -> Result<()> {
1687 block_on_compat(self.rmdir_path(path))
1688 }
1689
1690 /// Blocking version of [`Mount::rename`]
1691 fn sync_rename(
1692 &self,
1693 from_dir_fh: Bytes,
1694 from_filename: &str,
1695 to_dir_fh: Bytes,
1696 to_filename: &str,
1697 ) -> Result<()> {
1698 block_on_compat(self.rename(from_dir_fh, from_filename, to_dir_fh, to_filename))
1699 }
1700
1701 /// Blocking version of [`Mount::rename_path`]
1702 fn sync_rename_path(&self, from_path: &str, to_path: &str) -> Result<()> {
1703 block_on_compat(self.rename_path(from_path, to_path))
1704 }
1705
1706 /// Blocking version of [`Mount::umount`]
1707 fn sync_umount(&self) -> Result<()> {
1708 block_on_compat(self.umount())
1709 }
1710
1711 /// Procedure EXPORT returns the list of file systems exported by the server.
1712 ///
1713 /// Each entry contains the export path and the list of allowed client groups or hosts.
1714 /// An empty `groups` list means the export is accessible by any client (world-readable).
1715 /// Equivalent to `showmount -e`.
1716 ///
1717 /// # Example
1718 ///
1719 /// ```
1720 /// async fn print_exports(mount: &dyn nfs_rs::Mount) -> nfs_rs::Result<()> {
1721 /// for entry in mount.exports().await? {
1722 /// println!("{} {:?}", entry.path, entry.groups);
1723 /// }
1724 /// Ok(())
1725 /// }
1726 /// ```
1727 async fn exports(&self) -> Result<Vec<ExportEntry>>;
1728
1729 /// Blocking version of [`Mount::exports`]
1730 fn sync_exports(&self) -> Result<Vec<ExportEntry>> {
1731 block_on_compat(self.exports())
1732 }
1733}
1734
1735/// A single entry returned by [`Mount::exports`]: an exported path and the list of
1736/// allowed client groups (empty = unrestricted / world-accessible).
1737#[derive(Debug, Default, PartialEq, Clone)]
1738pub struct ExportEntry {
1739 pub path: String,
1740 pub groups: Vec<String>,
1741}
1742
1743#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1744pub enum NFSVersion {
1745 Unknown,
1746 NFSv3,
1747 NFSv4p0,
1748 /// Compatibility spelling retained for callers of releases before 0.5.0.
1749 /// URL parsing intentionally does not map the ambiguous selector `4` here.
1750 #[deprecated(note = "use NFSVersion::NFSv4p0 and the exact URL selector version=4.0")]
1751 NFSv4,
1752 NFSv4p1,
1753 NFSv4p2,
1754}
1755
1756impl From<&str> for NFSVersion {
1757 fn from(val: &str) -> Self {
1758 match val {
1759 "3" => NFSVersion::NFSv3,
1760 "4.0" => NFSVersion::NFSv4p0,
1761 "4.1" => NFSVersion::NFSv4p1,
1762 "4.2" => NFSVersion::NFSv4p2,
1763 _ => NFSVersion::Unknown,
1764 }
1765 }
1766}
1767
1768/// Struct describing attributes for an NFS entry.
1769#[derive(Debug, Default, PartialEq, Clone)]
1770pub struct Attr {
1771 pub type_: u32,
1772 pub file_mode: u32,
1773 pub nlink: u32,
1774 pub uid: u32,
1775 pub gid: u32,
1776 pub filesize: u64,
1777 pub used: u64,
1778 pub spec_data: [u32; 2],
1779 pub fsid: u64,
1780 pub fileid: u64,
1781 pub atime: Time,
1782 pub mtime: Time,
1783 pub ctime: Time,
1784 /// NFSv4 ACL decoded from FATTR4_ACL (attr #12). `None` for NFSv3 or if not requested.
1785 pub acl: Option<Acl>,
1786 /// NFSv4 raw owner string (attr #35), e.g. "root@localdomain" or "0".
1787 /// Empty string for NFSv3 or if not present.
1788 pub owner: String,
1789 /// NFSv4 raw owner_group string (attr #37 in NFSv4.1).
1790 pub owner_group: String,
1791 /// NFSv4.1 per-entry filehandle (FATTR4_FILEHANDLE, attr #19).
1792 /// Returned by READDIR when requested in the attr bitmap.
1793 /// Empty for NFSv3 or if not requested.
1794 pub filehandle: Bytes,
1795}
1796
1797/// NFSv4 ACE type (RFC 7530 §6.2.1.1).
1798#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1799#[repr(u32)]
1800pub enum AceType {
1801 AccessAllowed = 0,
1802 AccessDenied = 1,
1803 SystemAudit = 2,
1804 SystemAlarm = 3,
1805}
1806
1807/// NFSv4 ACE flags bitfield (RFC 7530 §6.2.1.4).
1808#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1809pub struct AceFlags(pub u32);
1810
1811impl AceFlags {
1812 pub const FILE_INHERIT: u32 = 0x0000_0001;
1813 pub const DIRECTORY_INHERIT: u32 = 0x0000_0002;
1814 pub const NO_PROPAGATE_INHERIT: u32 = 0x0000_0004;
1815 pub const INHERIT_ONLY: u32 = 0x0000_0008;
1816 pub const SUCCESSFUL_ACCESS: u32 = 0x0000_0010;
1817 pub const FAILED_ACCESS: u32 = 0x0000_0020;
1818 pub const IDENTIFIER_GROUP: u32 = 0x0000_0040;
1819 pub const INHERITED: u32 = 0x0000_0080;
1820
1821 pub fn contains(self, flag: u32) -> bool {
1822 self.0 & flag != 0
1823 }
1824}
1825
1826/// NFSv4 ACE access mask bitfield (RFC 7530 §6.2.1.3).
1827#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1828pub struct AceMask(pub u32);
1829
1830impl AceMask {
1831 pub const READ_DATA: u32 = 0x0000_0001;
1832 pub const LIST_DIRECTORY: u32 = 0x0000_0001;
1833 pub const WRITE_DATA: u32 = 0x0000_0002;
1834 pub const ADD_FILE: u32 = 0x0000_0002;
1835 pub const APPEND_DATA: u32 = 0x0000_0004;
1836 pub const ADD_SUBDIRECTORY: u32 = 0x0000_0004;
1837 pub const READ_NAMED_ATTRS: u32 = 0x0000_0008;
1838 pub const WRITE_NAMED_ATTRS: u32 = 0x0000_0010;
1839 pub const EXECUTE: u32 = 0x0000_0020;
1840 pub const DELETE_CHILD: u32 = 0x0000_0040;
1841 pub const READ_ATTRIBUTES: u32 = 0x0000_0080;
1842 pub const WRITE_ATTRIBUTES: u32 = 0x0000_0100;
1843 pub const DELETE: u32 = 0x0001_0000;
1844 pub const READ_ACL: u32 = 0x0002_0000;
1845 pub const WRITE_ACL: u32 = 0x0004_0000;
1846 pub const WRITE_OWNER: u32 = 0x0008_0000;
1847 pub const SYNCHRONIZE: u32 = 0x0010_0000;
1848
1849 pub fn contains(self, mask: u32) -> bool {
1850 self.0 & mask != 0
1851 }
1852}
1853
1854/// A single NFSv4 Access Control Entry (RFC 7530 §6.2.1).
1855#[derive(Debug, Clone, PartialEq, Eq)]
1856pub struct NfsAce {
1857 pub ace_type: AceType,
1858 pub flags: AceFlags,
1859 pub access_mask: AceMask,
1860 pub who: String,
1861}
1862
1863/// NFSv4 ACL: ordered list of access control entries.
1864#[derive(Debug, Clone, PartialEq, Eq, Default)]
1865pub struct Acl {
1866 pub aces: Vec<NfsAce>,
1867}
1868
1869/// NFSv4.1 ACL-wide flags carried by FATTR4_DACL and FATTR4_SACL.
1870#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1871pub struct Acl41Flags(pub u32);
1872
1873impl Acl41Flags {
1874 pub const AUTO_INHERIT: u32 = 0x0000_0001;
1875 pub const PROTECTED: u32 = 0x0000_0002;
1876 pub const DEFAULTED: u32 = 0x0000_0004;
1877
1878 pub fn contains(self, flag: u32) -> bool {
1879 self.0 & flag != 0
1880 }
1881}
1882
1883/// NFSv4.1 DACL/SACL value (`nfsacl41`): ACL-wide flags plus ordered ACEs.
1884#[derive(Debug, Clone, PartialEq, Eq, Default)]
1885pub struct NfsAcl41 {
1886 pub flags: Acl41Flags,
1887 pub aces: Vec<NfsAce>,
1888}
1889
1890/// Bitmask of ACE types supported by the server (FATTR4_ACLSUPPORT, attr #13).
1891#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1892pub struct AclSupport(pub u32);
1893
1894impl AclSupport {
1895 pub const ALLOW: u32 = 0x0000_0001;
1896 pub const DENY: u32 = 0x0000_0002;
1897 pub const AUDIT: u32 = 0x0000_0004;
1898 pub const ALARM: u32 = 0x0000_0008;
1899
1900 pub fn supports(self, ace_type: u32) -> bool {
1901 self.0 & ace_type != 0
1902 }
1903}
1904
1905/// Struct describing non-volatile file system state information.
1906#[derive(Debug, Default, PartialEq)]
1907pub struct FSInfo {
1908 pub attr: Option<Attr>,
1909 pub rtmax: u32,
1910 pub rtpref: u32,
1911 pub rtmult: u32,
1912 pub wtmax: u32,
1913 pub wtpref: u32,
1914 pub wtmult: u32,
1915 pub dtpref: u32,
1916 pub maxfilesize: u64,
1917 pub time_delta: Time,
1918 pub properties: u32,
1919}
1920
1921/// Struct describing volatile file system state information.
1922#[derive(Debug, Default, PartialEq)]
1923pub struct FSStat {
1924 pub attr: Option<Attr>,
1925 pub tbytes: u64,
1926 pub fbytes: u64,
1927 pub abytes: u64,
1928 pub tfiles: u64,
1929 pub ffiles: u64,
1930 pub afiles: u64,
1931 pub invarsec: u32,
1932}
1933
1934/// Struct describing an NFS entry response as returned by various procedures.
1935#[derive(Debug, Default, PartialEq, Clone)]
1936pub struct ObjRes {
1937 pub fh: Bytes,
1938 pub attr: Option<Attr>,
1939}
1940
1941/// Struct describing path configuration for an NFS entry as returned by [`Mount::pathconf`] and [`Mount::pathconf_path`].
1942#[derive(Debug, Default, PartialEq)]
1943pub struct Pathconf {
1944 pub attr: Option<Attr>,
1945 pub linkmax: u32,
1946 pub name_max: u32,
1947 pub no_trunc: bool,
1948 pub chown_restricted: bool,
1949 pub case_insensitive: bool,
1950 pub case_preserving: bool,
1951}
1952
1953/// Availability of the optional NFSv4 attributes represented by [`Pathconf`].
1954#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)]
1955pub struct PathconfSupport {
1956 pub linkmax: bool,
1957 pub name_max: bool,
1958 pub no_trunc: bool,
1959 pub chown_restricted: bool,
1960 pub case_insensitive: bool,
1961 pub case_preserving: bool,
1962}
1963
1964impl PathconfSupport {
1965 pub const fn all() -> Self {
1966 Self {
1967 linkmax: true,
1968 name_max: true,
1969 no_trunc: true,
1970 chown_restricted: true,
1971 case_insensitive: true,
1972 case_preserving: true,
1973 }
1974 }
1975}
1976
1977/// PATHCONF values plus explicit availability for optional NFSv4 attributes.
1978///
1979/// When an availability field is false, the corresponding value is a
1980/// conservative compatibility default and must not be interpreted as a
1981/// server-reported value.
1982#[derive(Debug, Default, PartialEq)]
1983pub struct SupportedPathconf {
1984 pub values: Pathconf,
1985 pub available: PathconfSupport,
1986 /// NFSv4 filesystem identifier that scopes `supported_attrs`.
1987 pub fsid: Option<(u64, u64)>,
1988}
1989
1990/// Struct describing a single NFS entry as returned by [`Mount::readdir`] and [`Mount::readdir_path`].
1991#[derive(Debug)]
1992pub struct ReaddirEntry {
1993 pub fileid: u64,
1994 pub file_name: String,
1995}
1996
1997/// Struct describing a single NFS entry as returned by [`Mount::readdirplus`] and [`Mount::readdirplus_path`].
1998#[derive(Debug)]
1999pub struct ReaddirplusEntry {
2000 pub fileid: u64,
2001 pub file_name: String,
2002 pub attr: Option<Attr>,
2003 pub handle: Bytes,
2004}
2005
2006/// Validate only acknowledgements that still require stable storage.
2007pub(crate) fn verify_write_batch(
2008 protocol: NFSVersion,
2009 writes: &[WriteOutcome],
2010 actual: Option<[u8; 8]>,
2011) -> Result<()> {
2012 for write in writes
2013 .iter()
2014 .filter(|w| w.committed != crate::WriteCommitted::FileSync)
2015 {
2016 if let Some(expected) = write.verifier
2017 && actual != Some(expected)
2018 {
2019 return Err(write_verifier_changed(protocol));
2020 }
2021 }
2022 Ok(())
2023}
2024
2025#[cfg(test)]
2026mod negotiated_size_tests {
2027 use super::*;
2028
2029 #[test]
2030 fn empty_read_requires_eof() {
2031 assert!(read_reply(Bytes::new(), false).is_err());
2032 assert!(read_reply(Bytes::new(), true).unwrap().is_empty());
2033 assert_eq!(
2034 read_reply(Bytes::from_static(b"short"), false).unwrap(),
2035 b"short"[..]
2036 );
2037 }
2038
2039 #[test]
2040 fn automatic_sizes_respect_server_and_client_limits() {
2041 assert_eq!(negotiated_io_size(4096).unwrap(), 4096);
2042 assert_eq!(
2043 negotiated_io_size(2 * 1024 * 1024).unwrap(),
2044 2 * 1024 * 1024
2045 );
2046 assert_eq!(negotiated_io_size(u64::MAX).unwrap(), MAX_IO_SIZE);
2047 assert!(negotiated_io_size(0).is_err());
2048 }
2049}
2050
2051#[cfg(test)]
2052mod write_committed_tests {
2053 use super::*;
2054
2055 #[test]
2056 fn response_levels_are_distinct_and_invalid_wire_values_fail() {
2057 for (wire, expected) in [
2058 (0, WriteCommitted::Unstable),
2059 (1, WriteCommitted::DataSync),
2060 (2, WriteCommitted::FileSync),
2061 ] {
2062 let outcome =
2063 WriteOutcome::new(7, WriteCommitted::try_from(wire).unwrap(), Some([9; 8]));
2064 assert_eq!(outcome.committed, expected);
2065 assert_eq!(outcome.count, 7);
2066 assert_eq!(outcome.verifier, Some([9; 8]));
2067 let verified = verify_write_batch(NFSVersion::NFSv3, &[outcome], Some([8; 8]));
2068 assert_eq!(verified.is_ok(), expected == WriteCommitted::FileSync);
2069 }
2070 assert!(WriteCommitted::try_from(3).is_err());
2071 assert!(WriteCommitted::try_from(u32::MAX).is_err());
2072 }
2073}