thin_status/status_code.rs
1// Copyright 2026 <https://github.com/ppetr/>
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15#[cfg(feature = "use_libc")]
16use libc;
17use std::fmt::Display;
18use std::num::NonZeroI32;
19use strum;
20
21/// `errno` constants that the `libc` crate does not expose on every target
22/// (many `E*` values are Linux-specific, and Windows only defines a small
23/// subset).
24///
25/// Where the real constant is missing we substitute a unique negative sentinel.
26/// Real `errno` values are positive, so the affected match arms in
27/// [`ErrorCode::from_errno`] simply never match on those targets.
28#[cfg(feature = "use_libc")]
29mod compat_errno {
30 use libc::c_int;
31
32 /// Present only on Linux / Android.
33 macro_rules! linux_errno {
34 ($($name:ident = $sentinel:literal,)*) => {$(
35 #[cfg(any(target_os = "linux", target_os = "android"))]
36 pub const $name: c_int = libc::$name;
37 #[cfg(not(any(target_os = "linux", target_os = "android")))]
38 pub const $name: c_int = -$sentinel;
39 )*};
40 }
41
42 /// Present on Unix-like targets but not on Windows.
43 macro_rules! unix_errno {
44 ($($name:ident = $sentinel:literal,)*) => {$(
45 #[cfg(unix)]
46 pub const $name: c_int = libc::$name;
47 #[cfg(not(unix))]
48 pub const $name: c_int = -$sentinel;
49 )*};
50 }
51
52 linux_errno! {
53 ENOMEDIUM = 1001,
54 ENOTUNIQ = 1002,
55 ENOKEY = 1003,
56 EBADFD = 1004,
57 EISNAM = 1005,
58 EUNATCH = 1006,
59 ECHRNG = 1007,
60 ENOPKG = 1008,
61 ECOMM = 1009,
62 ENONET = 1010,
63 }
64
65 unix_errno! {
66 ENOTBLK = 1101,
67 ESHUTDOWN = 1102,
68 EDQUOT = 1103,
69 EUSERS = 1104,
70 EPFNOSUPPORT = 1105,
71 ESOCKTNOSUPPORT = 1106,
72 EHOSTDOWN = 1107,
73 ESTALE = 1108,
74 }
75}
76
77/// Derived from <https://github.com/abseil/abseil-cpp/blob/master/absl/status/status.h>. See the
78/// link for more information.
79///
80/// (Copyright 2019 The Abseil Authors.)
81///
82/// Unlike `absl::StatusCode`, this enum only allows representing non-OK values.
83#[derive(
84 Clone,
85 Copy,
86 Debug,
87 Eq,
88 Hash,
89 Ord,
90 PartialEq,
91 PartialOrd,
92 strum::EnumString,
93 strum::EnumIter,
94 strum::FromRepr,
95 strum::IntoStaticStr,
96 strum::VariantArray,
97)]
98#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
99#[repr(i32)]
100#[non_exhaustive]
101pub enum ErrorCode {
102 /// `Cancelled` (gRPC code "CANCELLED") indicates the operation was cancelled, typically by the
103 /// caller.
104 Cancelled = 1,
105
106 /// `Unknown` (gRPC code "UNKNOWN") indicates an unknown error occurred. In general, more
107 /// specific errors should be raised, if possible. Errors raised by APIs that do not return
108 /// enough error information may be converted to this error.
109 Unknown = 2,
110
111 /// `InvalidArgument` (gRPC code "INVALID_ARGUMENT") indicates the caller specified an invalid
112 /// argument, such as a malformed filename. Note that use of such errors should be narrowly
113 /// limited to indicate the invalid nature of the arguments themselves. Errors with validly
114 /// formed arguments that may cause errors with the state of the receiving system should be
115 /// denoted with `FailedPrecondition` instead.
116 InvalidArgument = 3,
117
118 /// `DeadlineExceeded` (gRPC code "DEADLINE_EXCEEDED") indicates a deadline expired before the
119 /// operation could complete. For operations that may change state within a system, this error
120 /// may be returned even if the operation has completed successfully. For example, a successful
121 /// response from a server could have been delayed long enough for the deadline to expire.
122 DeadlineExceeded = 4,
123
124 /// `NotFound` (gRPC code "NOT_FOUND") indicates some requested entity (such as a file or
125 /// directory) was not found.
126 ///
127 /// `NotFound` is useful if a request should be denied for an entire class of users, such as
128 /// during a gradual feature rollout or undocumented allow list. If a request should be denied
129 /// for specific sets of users, such as through user-based access control, use
130 /// `PermissionDenied` instead.
131 NotFound = 5,
132
133 /// `AlreadyExists` (gRPC code "ALREADY_EXISTS") indicates that the entity a caller attempted to
134 /// create (such as a file or directory) is already present.
135 AlreadyExists = 6,
136
137 /// `PermissionDenied` (gRPC code "PERMISSION_DENIED") indicates that the caller does not have
138 /// permission to execute the specified operation. Note that this error is different than an
139 /// error due to an *un*authenticated user. This error code does not imply the request is valid
140 /// or the requested entity exists or satisfies any other pre-conditions.
141 ///
142 /// `PermissionDenied` must not be used for rejections caused by exhausting some resource.
143 /// Instead, use `ResourceExhausted` for those errors. `PermissionDenied` must not be used if
144 /// the caller cannot be identified. Instead, use `Unauthenticated` for those errors.
145 PermissionDenied = 7,
146
147 /// `ResourceExhausted` (gRPC code "RESOURCE_EXHAUSTED") indicates some resource has been
148 /// exhausted, perhaps a per-user quota, or perhaps the entire file system is out of space.
149 ResourceExhausted = 8,
150
151 /// `FailedPrecondition` (gRPC code "FAILED_PRECONDITION") indicates that the operation was
152 /// rejected because the system is not in a state required for the operation's execution. For
153 /// example, a directory to be deleted may be non-empty, an "rmdir" operation is applied to a
154 /// non-directory, etc.
155 ///
156 /// Some guidelines that may help a service implementer in deciding between
157 /// `FailedPrecondition`, `Aborted`, and `Unavailable`:
158 ///
159 /// 1. Use `Unavailable` if the client can retry just the failing call.
160 /// 2. Use `Aborted` if the client should retry at a higher transaction level (such as when a
161 /// client-specified test-and-set fails, indicating the client should restart a
162 /// read-modify-write sequence).
163 /// 3. Use `FailedPrecondition` if the client should not retry until the system state has
164 /// been explicitly fixed. For example, if a "rmdir" fails because the directory is
165 /// non-empty, `FailedPrecondition` should be returned since the client should not retry
166 /// unless the files are deleted from the directory.
167 FailedPrecondition = 9,
168
169 /// `Aborted` (gRPC code "ABORTED") indicates the operation was aborted, typically due to a
170 /// concurrency issue such as a sequencer check failure or a failed transaction.
171 ///
172 /// See the guidelines above for deciding between `FailedPrecondition`, `Aborted`, and
173 /// `Unavailable`.
174 Aborted = 10,
175
176 /// `OutOfRange` (gRPC code "OUT_OF_RANGE") indicates the operation was attempted past the valid
177 /// range, such as seeking or reading past an end-of-file.
178 ///
179 /// Unlike `InvalidArgument`, this error indicates a problem that may be fixed if the system
180 /// state changes. For example, a 32-bit file system will generate `InvalidArgument` if asked
181 /// to read at an offset that is not in the range [0,2^32-1], but it will generate `OutOfRange`
182 /// if asked to read from an offset past the current file size.
183 ///
184 /// There is a fair bit of overlap between `FailedPrecondition` and `OutOfRange`. We recommend
185 /// using `OutOfRange` (the more specific error) when it applies so that callers who are
186 /// iterating through a space can easily look for an `OutOfRange` error to detect when they are
187 /// done.
188 OutOfRange = 11,
189
190 /// `Unimplemented` (gRPC code "UNIMPLEMENTED") indicates the operation is not implemented or
191 /// supported in this service. In this case, the operation should not be re-attempted.
192 Unimplemented = 12,
193
194 /// `Internal` (gRPC code "INTERNAL") indicates an internal error has occurred and some
195 /// invariants expected by the underlying system have not been satisfied. This error code is
196 /// reserved for serious errors.
197 Internal = 13,
198
199 /// `Unavailable` (gRPC code "UNAVAILABLE") indicates the service is currently unavailable and
200 /// that this is most likely a transient condition. An error such as this can be corrected by
201 /// retrying with a backoff scheme. Note that it is not always safe to retry non-idempotent
202 /// operations.
203 ///
204 /// See the guidelines above for deciding between `FailedPrecondition`, `Aborted`, and
205 /// `Unavailable`.
206 Unavailable = 14,
207
208 /// `DataLoss` (gRPC code "DATA_LOSS") indicates that unrecoverable data loss or corruption has
209 /// occurred. As this error is serious, proper alerting should be attached to errors such as
210 /// this.
211 DataLoss = 15,
212
213 /// `Unauthenticated` (gRPC code "UNAUTHENTICATED") indicates that the request does not have
214 /// valid authentication credentials for the operation. Correct the authentication and try
215 /// again.
216 Unauthenticated = 16,
217}
218
219impl ErrorCode {
220 /// Adapted from `ErrnoToStatusCode` in
221 /// <https://github.com/abseil/abseil-cpp/blob/master/absl/status/status.cc>
222 #[cfg(feature = "use_libc")]
223 pub fn from_errno(errno: i32) -> Option<ErrorCode> {
224 match errno {
225 0 => None,
226 libc::EINVAL | // Invalid argument
227 libc::ENAMETOOLONG | // Filename too long
228 libc::E2BIG | // Argument list too long
229 libc::EDESTADDRREQ | // Destination address required
230 libc::EDOM | // Mathematics argument out of domain of function
231 libc::EFAULT | // Bad address
232 libc::EILSEQ | // Illegal byte sequence
233 libc::ENOPROTOOPT | // Protocol not available
234 libc::ENOTSOCK | // Not a socket
235 libc::ENOTTY | // Inappropriate I/O control operation
236 libc::EPROTOTYPE | // Protocol wrong type for socket
237 libc::ESPIPE => // Invalid seek
238 Some(Self::InvalidArgument),
239 libc::ETIMEDOUT => // Connection timed out
240 Some(Self::DeadlineExceeded),
241 libc::ENODEV | // No such device
242 libc::ENOENT | // No such file or directory
243 compat_errno::ENOMEDIUM | // No medium found
244 libc::ENXIO | // No such device or address
245 libc::ESRCH => // No such process
246 Some(Self::NotFound),
247 libc::EEXIST | // File exists
248 libc::EADDRNOTAVAIL | // Address not available
249 libc::EALREADY | // Connection already in progress
250 compat_errno::ENOTUNIQ => // Name not unique on network
251 Some(Self::AlreadyExists),
252 libc::EPERM | // Operation not permitted
253 libc::EACCES | // Permission denied
254 compat_errno::ENOKEY | // Required key not available
255 libc::EROFS => // Read only file system
256 Some(Self::PermissionDenied),
257 libc::ENOTEMPTY | // Directory not empty
258 libc::EISDIR | // Is a directory
259 libc::ENOTDIR | // Not a directory
260 libc::EADDRINUSE | // Address already in use
261 libc::EBADF | // Invalid file descriptor
262 compat_errno::EBADFD | // File descriptor in bad state
263 libc::EBUSY | // Device or resource busy
264 libc::ECHILD | // No child processes
265 libc::EISCONN | // Socket is connected
266 compat_errno::EISNAM | // Is a named type file
267 compat_errno::ENOTBLK | // Block device required
268 libc::ENOTCONN | // The socket is not connected
269 libc::EPIPE | // Broken pipe
270 compat_errno::ESHUTDOWN | // Cannot send after transport endpoint shutdown
271 libc::ETXTBSY | // Text file busy
272 compat_errno::EUNATCH => // Protocol driver not attached
273 Some(Self::FailedPrecondition),
274 libc::ENOSPC | // No space left on device
275 compat_errno::EDQUOT | // Disk quota exceeded
276 libc::EMFILE | // Too many open files
277 libc::EMLINK | // Too many links
278 libc::ENFILE | // Too many open files in system
279 libc::ENOBUFS | // No buffer space available
280 libc::ENOMEM | // Not enough space
281 compat_errno::EUSERS => // Too many users
282 Some(Self::ResourceExhausted),
283 compat_errno::ECHRNG | // Channel number out of range
284 libc::EFBIG | // File too large
285 libc::EOVERFLOW | // Value too large to be stored in data type
286 libc::ERANGE => // Result too large
287 Some(Self::OutOfRange),
288 compat_errno::ENOPKG | // Package not installed
289 libc::ENOSYS | // Function not implemented
290 libc::ENOTSUP | // Operation not supported
291 libc::EAFNOSUPPORT | // Address family not supported
292 compat_errno::EPFNOSUPPORT | // Protocol family not supported
293 libc::EPROTONOSUPPORT | // Protocol not supported
294 compat_errno::ESOCKTNOSUPPORT | // Socket type not supported
295 libc::EXDEV => // Improper link
296 Some(Self::Unimplemented),
297 libc::EAGAIN | // Resource temporarily unavailable
298 compat_errno::ECOMM | // Communication error on send
299 libc::ECONNREFUSED | // Connection refused
300 libc::ECONNABORTED | // Connection aborted
301 libc::ECONNRESET | // Connection reset
302 libc::EINTR | // Interrupted function call
303 compat_errno::EHOSTDOWN | // Host is down
304 libc::EHOSTUNREACH | // Host is unreachable
305 libc::ENETDOWN | // Network is down
306 libc::ENETRESET | // Connection aborted by network
307 libc::ENETUNREACH | // Network unreachable
308 libc::ENOLCK | // No locks available
309 libc::ENOLINK | // Link has been severed
310 compat_errno::ENONET => // Machine is not on the network
311 Some(Self::Unavailable),
312 libc::EDEADLK | // Resource deadlock avoided
313 compat_errno::ESTALE => // Stale file handle
314 Some(Self::Aborted),
315 libc::ECANCELED => // Operation cancelled
316 Some(Self::Cancelled),
317 _ => Some(Self::Unknown),
318 }
319 }
320
321 pub fn from_error_kind(kind: std::io::ErrorKind) -> ErrorCode {
322 use std::io::ErrorKind::*;
323 match kind {
324 NotFound => ErrorCode::NotFound,
325
326 PermissionDenied => ErrorCode::PermissionDenied,
327
328 AlreadyExists => ErrorCode::AlreadyExists,
329
330 InvalidInput | ArgumentListTooLong => ErrorCode::InvalidArgument,
331
332 TimedOut => ErrorCode::DeadlineExceeded,
333
334 StorageFull | QuotaExceeded | OutOfMemory => ErrorCode::ResourceExhausted,
335
336 DirectoryNotEmpty | IsADirectory | NotADirectory | NotConnected | AddrInUse
337 | ResourceBusy | ExecutableFileBusy | ReadOnlyFilesystem // | FilesystemLoop
338 | CrossesDevices | NotSeekable => ErrorCode::FailedPrecondition,
339
340 FileTooLarge => ErrorCode::OutOfRange,
341
342 Unsupported => ErrorCode::Unimplemented,
343
344 WouldBlock | Interrupted | BrokenPipe | ConnectionRefused | ConnectionReset
345 | ConnectionAborted | HostUnreachable | NetworkUnreachable | NetworkDown => {
346 ErrorCode::Unavailable
347 }
348
349 Deadlock | StaleNetworkFileHandle => ErrorCode::Aborted,
350
351 InvalidData | WriteZero | UnexpectedEof => ErrorCode::DataLoss,
352
353 Other => ErrorCode::Unknown,
354
355 _ => ErrorCode::Unknown,
356 }
357 }
358
359 /// If `err` contains a `raw_os_error()`, returns it converted into an `ErrorCode`.
360 /// Otherwise returns `None`.
361 #[cfg(feature = "use_libc")]
362 pub fn from_raw_os_error(err: &std::io::Error) -> Option<ErrorCode> {
363 err.raw_os_error().and_then(Self::from_errno)
364 }
365}
366
367impl From<ErrorCode> for NonZeroI32 {
368 fn from(code: ErrorCode) -> Self {
369 NonZeroI32::new(code as i32).expect(&format!(
370 "The enum value of an ErrorCode must be nonzero, but got '{:?}'",
371 code
372 ))
373 }
374}
375
376/// If a value matches one of the defined error codes, returns it as an `ErrorCode`, otherwise
377/// returns a `()` error.
378///
379/// For converting from strings, use the `std::str::FromStr` instance.
380impl TryFrom<i32> for ErrorCode {
381 type Error = ();
382
383 fn try_from(code: i32) -> Result<Self, ()> {
384 Self::from_repr(code).ok_or(())
385 }
386}
387
388/// If the `alternate` flag is set, prints the error code as a number, otherwise as text.
389impl Display for ErrorCode {
390 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
391 if f.alternate() {
392 write!(f, "{}", *self as i32)
393 } else {
394 f.write_str(self.into())
395 }
396 }
397}
398
399#[cfg(test)]
400mod thin_status_tests {
401 use super::*;
402 use anyhow;
403
404 #[cfg(feature = "use_libc")]
405 #[test]
406 fn test_cloud_rpc_status_conversions() {
407 assert_eq!(ErrorCode::from_errno(0), None);
408 assert_eq!(
409 ErrorCode::from_errno(libc::ENOENT),
410 Some(ErrorCode::NotFound)
411 );
412 }
413
414 #[test]
415 fn test_to_string() {
416 assert_eq!(format!("{}", ErrorCode::NotFound), "NOT_FOUND");
417 assert_eq!(format!("{:#}", ErrorCode::NotFound), "5");
418 }
419
420 #[test]
421 fn test_anyhow_context() {
422 let err = anyhow::anyhow!("test error").context(ErrorCode::PermissionDenied);
423 assert_eq!(
424 err.downcast_ref::<ErrorCode>(),
425 Some(&ErrorCode::PermissionDenied)
426 );
427 }
428}