Skip to main content

zerofs_client/
error.rs

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