Skip to main content

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}