Skip to main content

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}