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 /// Any other server errno, preserved verbatim.
86 Io {
87 /// The Linux errno the server returned.
88 errno: i32,
89 /// The path the operation targeted (lossy display).
90 path: String,
91 /// Human-readable rendering of the errno.
92 message: String,
93 },
94 /// Wire-level failure: codec error, unexpected reply type, failed negotiation.
95 Protocol {
96 /// What went wrong on the wire.
97 message: String,
98 },
99}
100
101impl std::fmt::Display for ZeroFsError {
102 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103 match self {
104 Self::NotFound { path } => write!(f, "not found: {path}"),
105 Self::PermissionDenied { path } => write!(f, "permission denied: {path}"),
106 Self::NotPermitted { path } => write!(f, "operation not permitted: {path}"),
107 Self::AlreadyExists { path } => write!(f, "already exists: {path}"),
108 Self::NotADirectory { path } => write!(f, "not a directory: {path}"),
109 Self::IsADirectory { path } => write!(f, "is a directory: {path}"),
110 Self::DirectoryNotEmpty { path } => write!(f, "directory not empty: {path}"),
111 Self::NameTooLong { name } => write!(f, "name too long: {name}"),
112 Self::InvalidArgument { message } => write!(f, "invalid argument: {message}"),
113 Self::TooManySymlinks { path } => {
114 write!(f, "too many levels of symbolic links: {path}")
115 }
116 Self::Closed => write!(f, "handle is closed"),
117 Self::ConnectFailed { message } => write!(f, "connection failed: {message}"),
118 Self::NotLeader { path } => write!(f, "not the leader (re-route): {path}"),
119 Self::Io {
120 errno,
121 path,
122 message,
123 } => write!(f, "i/o error (errno {errno}): {path}: {message}"),
124 Self::Protocol { message } => write!(f, "protocol error: {message}"),
125 }
126 }
127}
128
129impl std::error::Error for ZeroFsError {}
130
131/// Lossless conversion for interop with std/tokio I/O: the errno round-trips
132/// through `from_raw_os_error` (so `ErrorKind` is set), and the original
133/// `ZeroFsError` is preserved as the source (recoverable via `downcast`).
134impl From<ZeroFsError> for std::io::Error {
135 fn from(e: ZeroFsError) -> Self {
136 let kind = std::io::Error::from_raw_os_error(e.to_errno()).kind();
137 std::io::Error::new(kind, e)
138 }
139}
140
141impl ZeroFsError {
142 /// Linux errno per the strict 1:1 table; `Io` returns its errno unchanged.
143 pub fn to_errno(&self) -> i32 {
144 match self {
145 Self::NotFound { .. } => libc::ENOENT,
146 Self::PermissionDenied { .. } => libc::EACCES,
147 Self::NotPermitted { .. } => libc::EPERM,
148 Self::AlreadyExists { .. } => libc::EEXIST,
149 Self::NotADirectory { .. } => libc::ENOTDIR,
150 Self::IsADirectory { .. } => libc::EISDIR,
151 Self::DirectoryNotEmpty { .. } => libc::ENOTEMPTY,
152 Self::NameTooLong { .. } => libc::ENAMETOOLONG,
153 Self::InvalidArgument { .. } => libc::EINVAL,
154 Self::TooManySymlinks { .. } => libc::ELOOP,
155 Self::Closed => libc::EBADF,
156 Self::ConnectFailed { .. } => libc::EIO,
157 Self::NotLeader { .. } => P9_ENOTLEADER as i32,
158 Self::Io { errno, .. } => *errno,
159 Self::Protocol { .. } => libc::EIO,
160 }
161 }
162
163 /// Map a server errno onto the variant table, keeping `path` as context.
164 pub(crate) fn from_errno(errno: i32, path: &str) -> Self {
165 let path = path.to_string();
166 match errno {
167 libc::ENOENT => Self::NotFound { path },
168 libc::EACCES => Self::PermissionDenied { path },
169 libc::EPERM => Self::NotPermitted { path },
170 libc::EEXIST => Self::AlreadyExists { path },
171 libc::ENOTDIR => Self::NotADirectory { path },
172 libc::EISDIR => Self::IsADirectory { path },
173 libc::ENOTEMPTY => Self::DirectoryNotEmpty { path },
174 libc::ENAMETOOLONG => Self::NameTooLong { name: path },
175 libc::EINVAL => Self::InvalidArgument {
176 message: format!("{path}: invalid argument"),
177 },
178 libc::ELOOP => Self::TooManySymlinks { path },
179 c if c == P9_ENOTLEADER as i32 => Self::NotLeader { path },
180 errno => Self::Io {
181 message: std::io::Error::from_raw_os_error(errno).to_string(),
182 errno,
183 path,
184 },
185 }
186 }
187
188 /// Map a transport-level result onto the public error surface. Server
189 /// errnos go through the variant table; everything else (codec errors,
190 /// unexpected replies) is a wire-level failure.
191 pub(crate) fn from_client(e: &ClientError, path: &str) -> Self {
192 match e {
193 ClientError::Errno(code) => Self::from_errno(*code as i32, path),
194 other => Self::Protocol {
195 message: format!("{path}: {other}"),
196 },
197 }
198 }
199}
200
201/// Attach path context while mapping a transport result onto the public error.
202pub(crate) trait ClientResultExt<T> {
203 fn ctx(self, path: &str) -> Result<T, ZeroFsError>;
204}
205
206impl<T> ClientResultExt<T> for Result<T, ClientError> {
207 fn ctx(self, path: &str) -> Result<T, ZeroFsError> {
208 self.map_err(|e| ZeroFsError::from_client(&e, path))
209 }
210}