Skip to main content

zerofs_client/
error.rs

1use ninep_client::ClientError;
2use ninep_proto::P9_ENOTLEADER;
3
4/// Flat, exhaustive error type. New variants require matching updates in
5/// `zerofs-ffi`.
6///
7/// `path` and `name` payloads are lossy display strings and are not inputs.
8///
9/// Unclassified server errnos retain their numeric value in [`ZeroFsError::Io`].
10#[derive(Debug, Clone)]
11pub enum ZeroFsError {
12    /// No entry at the path (ENOENT).
13    NotFound {
14        /// The path the operation targeted (lossy display).
15        path: String,
16    },
17    /// Access denied by permission bits (EACCES).
18    PermissionDenied {
19        /// The path the operation targeted (lossy display).
20        path: String,
21    },
22    /// EPERM, distinct from EACCES: the operation requires ownership or
23    /// privilege (e.g. chown by a non-owner).
24    NotPermitted {
25        /// The path the operation targeted (lossy display).
26        path: String,
27    },
28    /// The target already exists (EEXIST).
29    AlreadyExists {
30        /// The path the operation targeted (lossy display).
31        path: String,
32    },
33    /// A path component is not a directory (ENOTDIR).
34    NotADirectory {
35        /// The path the operation targeted (lossy display).
36        path: String,
37    },
38    /// The target is a directory where a non-directory was required (EISDIR).
39    IsADirectory {
40        /// The path the operation targeted (lossy display).
41        path: String,
42    },
43    /// A directory removal found the directory non-empty (ENOTEMPTY).
44    DirectoryNotEmpty {
45        /// The path the operation targeted (lossy display).
46        path: String,
47    },
48    /// Name exceeds 255 bytes.
49    NameTooLong {
50        /// The offending name (lossy display).
51        name: String,
52    },
53    /// Bad input detected client-side (e.g. `..` component, conflicting open
54    /// options) or EINVAL from the server.
55    InvalidArgument {
56        /// What was wrong with the input.
57        message: String,
58    },
59    /// Symlink resolution exceeded the 40-hop cap (cycle or pathological chain).
60    TooManySymlinks {
61        /// The path whose resolution looped (lossy display).
62        path: String,
63    },
64    /// Handle or client used after `close()`.
65    Closed,
66    /// Initial connection, negotiation, or attach failure.
67    ConnectFailed {
68        /// What failed during connect/attach.
69        message: String,
70    },
71    /// The target is no longer the HA leader (`P9_ENOTLEADER`).
72    NotLeader {
73        /// The path the operation targeted (lossy display).
74        path: String,
75    },
76    /// Stale handle (`ESTALE`). From sync methods, prior acknowledged writes may
77    /// be non-durable and require replacement. Other operations require reopen.
78    Stale {
79        /// The path the operation targeted (lossy display).
80        path: String,
81    },
82    /// Any other server errno, preserved verbatim.
83    Io {
84        /// The Linux errno the server returned.
85        errno: i32,
86        /// The path the operation targeted (lossy display).
87        path: String,
88        /// Human-readable rendering of the errno.
89        message: String,
90    },
91    /// Wire-level failure: codec error, unexpected reply type, failed negotiation.
92    Protocol {
93        /// What went wrong on the wire.
94        message: String,
95    },
96}
97
98impl std::fmt::Display for ZeroFsError {
99    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100        match self {
101            Self::NotFound { path } => write!(f, "not found: {path}"),
102            Self::PermissionDenied { path } => write!(f, "permission denied: {path}"),
103            Self::NotPermitted { path } => write!(f, "operation not permitted: {path}"),
104            Self::AlreadyExists { path } => write!(f, "already exists: {path}"),
105            Self::NotADirectory { path } => write!(f, "not a directory: {path}"),
106            Self::IsADirectory { path } => write!(f, "is a directory: {path}"),
107            Self::DirectoryNotEmpty { path } => write!(f, "directory not empty: {path}"),
108            Self::NameTooLong { name } => write!(f, "name too long: {name}"),
109            Self::InvalidArgument { message } => write!(f, "invalid argument: {message}"),
110            Self::TooManySymlinks { path } => {
111                write!(f, "too many levels of symbolic links: {path}")
112            }
113            Self::Closed => write!(f, "handle is closed"),
114            Self::ConnectFailed { message } => write!(f, "connection failed: {message}"),
115            Self::NotLeader { path } => write!(f, "not the leader (re-route): {path}"),
116            Self::Stale { path } => {
117                write!(
118                    f,
119                    "stale handle (fsync: prior writes may not be durable): {path}"
120                )
121            }
122            Self::Io {
123                errno,
124                path,
125                message,
126            } => write!(f, "i/o error (errno {errno}): {path}: {message}"),
127            Self::Protocol { message } => write!(f, "protocol error: {message}"),
128        }
129    }
130}
131
132impl std::error::Error for ZeroFsError {}
133
134/// Lossless conversion for interop with std/tokio I/O: the errno round-trips
135/// through `from_raw_os_error` (so `ErrorKind` is set), and the original
136/// `ZeroFsError` is preserved as the source (recoverable via `downcast`).
137impl From<ZeroFsError> for std::io::Error {
138    fn from(e: ZeroFsError) -> Self {
139        let kind = std::io::Error::from_raw_os_error(e.to_errno()).kind();
140        std::io::Error::new(kind, e)
141    }
142}
143
144impl ZeroFsError {
145    /// Linux errno per the strict 1:1 table; `Io` returns its errno unchanged.
146    pub fn to_errno(&self) -> i32 {
147        match self {
148            Self::NotFound { .. } => crate::linux::ENOENT,
149            Self::PermissionDenied { .. } => crate::linux::EACCES,
150            Self::NotPermitted { .. } => crate::linux::EPERM,
151            Self::AlreadyExists { .. } => crate::linux::EEXIST,
152            Self::NotADirectory { .. } => crate::linux::ENOTDIR,
153            Self::IsADirectory { .. } => crate::linux::EISDIR,
154            Self::DirectoryNotEmpty { .. } => crate::linux::ENOTEMPTY,
155            Self::NameTooLong { .. } => crate::linux::ENAMETOOLONG,
156            Self::InvalidArgument { .. } => crate::linux::EINVAL,
157            Self::TooManySymlinks { .. } => crate::linux::ELOOP,
158            Self::Closed => crate::linux::EBADF,
159            Self::ConnectFailed { .. } => crate::linux::EIO,
160            Self::NotLeader { .. } => P9_ENOTLEADER as i32,
161            Self::Stale { .. } => crate::linux::ESTALE,
162            Self::Io { errno, .. } => *errno,
163            Self::Protocol { .. } => crate::linux::EIO,
164        }
165    }
166
167    /// Map a server errno onto the variant table, keeping `path` as context.
168    pub(crate) fn from_errno(errno: i32, path: &str) -> Self {
169        let path = path.to_string();
170        match errno {
171            crate::linux::ENOENT => Self::NotFound { path },
172            crate::linux::EACCES => Self::PermissionDenied { path },
173            crate::linux::EPERM => Self::NotPermitted { path },
174            crate::linux::EEXIST => Self::AlreadyExists { path },
175            crate::linux::ENOTDIR => Self::NotADirectory { path },
176            crate::linux::EISDIR => Self::IsADirectory { path },
177            crate::linux::ENOTEMPTY => Self::DirectoryNotEmpty { path },
178            crate::linux::ENAMETOOLONG => Self::NameTooLong { name: path },
179            crate::linux::EINVAL => Self::InvalidArgument {
180                message: format!("{path}: invalid argument"),
181            },
182            crate::linux::ELOOP => Self::TooManySymlinks { path },
183            crate::linux::ESTALE => Self::Stale { path },
184            c if c == P9_ENOTLEADER as i32 => Self::NotLeader { path },
185            errno => Self::Io {
186                message: std::io::Error::from_raw_os_error(errno).to_string(),
187                errno,
188                path,
189            },
190        }
191    }
192
193    /// Map a transport-level result onto the public error surface. Server
194    /// errnos go through the variant table; everything else (codec errors,
195    /// unexpected replies) is a wire-level failure.
196    pub(crate) fn from_client(e: &ClientError, path: &str) -> Self {
197        match e {
198            ClientError::Errno(code) => Self::from_errno(*code as i32, path),
199            other => Self::Protocol {
200                message: format!("{path}: {other}"),
201            },
202        }
203    }
204}
205
206/// Attach path context while mapping a transport result onto the public error.
207pub(crate) trait ClientResultExt<T> {
208    fn ctx(self, path: &str) -> Result<T, ZeroFsError>;
209}
210
211impl<T> ClientResultExt<T> for Result<T, ClientError> {
212    fn ctx(self, path: &str) -> Result<T, ZeroFsError> {
213        self.map_err(|e| ZeroFsError::from_client(&e, path))
214    }
215}