Skip to main content

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}