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