Skip to main content

oino_tools/env/
error.rs

1//! Node-style file system and spawn errors.
2
3use std::fmt;
4use std::io;
5
6/// A file system error rendered like Node: `{code}: {description}, {syscall} '{path}'`.
7#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
8pub struct FsError {
9    /// Errno name, e.g. `ENOENT`.
10    pub code: String,
11    /// libuv description, e.g. `no such file or directory`.
12    pub description: String,
13    /// Failing call, e.g. `open`.
14    pub syscall: String,
15    /// Path the call was made with (Node omits it for `read`).
16    pub path: Option<String>,
17}
18
19impl fmt::Display for FsError {
20    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
21        write!(f, "{}: {}, {}", self.code, self.description, self.syscall)?;
22        if let Some(path) = &self.path {
23            write!(f, " '{path}'")?;
24        }
25        Ok(())
26    }
27}
28
29impl FsError {
30    /// An error with an explicit errno name.
31    #[must_use]
32    pub fn new(code: &str, syscall: &str, path: Option<&str>) -> Self {
33        Self {
34            code: code.to_owned(),
35            description: describe(code).to_owned(),
36            syscall: syscall.to_owned(),
37            path: path.map(str::to_owned),
38        }
39    }
40
41    /// Converts an I/O error raised by `syscall` on `path`.
42    #[must_use]
43    pub fn from_io(error: &io::Error, syscall: &str, path: Option<&str>) -> Self {
44        Self::new(errno_name(error), syscall, path)
45    }
46}
47
48/// Failure to start a process (Node `spawn {program} {code}`).
49#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
50#[error("spawn {program} {code}")]
51pub struct SpawnError {
52    /// Program that failed to start.
53    pub program: String,
54    /// Errno name.
55    pub code: String,
56}
57
58impl SpawnError {
59    /// Converts an I/O error raised while starting `program`.
60    #[must_use]
61    pub fn from_io(error: &io::Error, program: &str) -> Self {
62        Self {
63            program: program.to_owned(),
64            code: errno_name(error).to_owned(),
65        }
66    }
67}
68
69/// The errno name of an I/O error (`UNKNOWN` when the OS code is not recognised).
70fn errno_name(error: &io::Error) -> &'static str {
71    #[cfg(unix)]
72    if let Some(name) = error.raw_os_error().and_then(unix_errno_name) {
73        return name;
74    }
75    match error.kind() {
76        io::ErrorKind::NotFound => "ENOENT",
77        io::ErrorKind::PermissionDenied => "EACCES",
78        io::ErrorKind::AlreadyExists => "EEXIST",
79        io::ErrorKind::IsADirectory => "EISDIR",
80        io::ErrorKind::NotADirectory => "ENOTDIR",
81        io::ErrorKind::DirectoryNotEmpty => "ENOTEMPTY",
82        io::ErrorKind::ReadOnlyFilesystem => "EROFS",
83        io::ErrorKind::StorageFull => "ENOSPC",
84        io::ErrorKind::InvalidFilename => "ENAMETOOLONG",
85        io::ErrorKind::InvalidInput => "EINVAL",
86        io::ErrorKind::ResourceBusy => "EBUSY",
87        _ => "UNKNOWN",
88    }
89}
90
91#[cfg(unix)]
92fn unix_errno_name(raw: i32) -> Option<&'static str> {
93    use nix::errno::Errno;
94    let name = match Errno::from_raw(raw) {
95        Errno::ENOENT => "ENOENT",
96        Errno::EACCES => "EACCES",
97        Errno::EEXIST => "EEXIST",
98        Errno::EISDIR => "EISDIR",
99        Errno::ENOTDIR => "ENOTDIR",
100        Errno::ENOTEMPTY => "ENOTEMPTY",
101        Errno::EPERM => "EPERM",
102        Errno::ENOSPC => "ENOSPC",
103        Errno::EROFS => "EROFS",
104        Errno::ELOOP => "ELOOP",
105        Errno::ENAMETOOLONG => "ENAMETOOLONG",
106        Errno::EBUSY => "EBUSY",
107        Errno::EMFILE => "EMFILE",
108        Errno::EINVAL => "EINVAL",
109        Errno::EIO => "EIO",
110        Errno::EXDEV => "EXDEV",
111        Errno::ETXTBSY => "ETXTBSY",
112        Errno::EAGAIN => "EAGAIN",
113        Errno::EBADF => "EBADF",
114        _ => return None,
115    };
116    Some(name)
117}
118
119/// libuv `uv_strerror` texts for the codes above.
120fn describe(code: &str) -> &'static str {
121    match code {
122        "ENOENT" => "no such file or directory",
123        "EACCES" => "permission denied",
124        "EEXIST" => "file already exists",
125        "EISDIR" => "illegal operation on a directory",
126        "ENOTDIR" => "not a directory",
127        "ENOTEMPTY" => "directory not empty",
128        "EPERM" => "operation not permitted",
129        "ENOSPC" => "no space left on device",
130        "EROFS" => "read-only file system",
131        "ELOOP" => "too many symbolic links encountered",
132        "ENAMETOOLONG" => "name too long",
133        "EBUSY" => "resource busy or locked",
134        "EMFILE" => "too many open files",
135        "EINVAL" => "invalid argument",
136        "EIO" => "i/o error",
137        "EXDEV" => "cross-device link not permitted",
138        "ETXTBSY" => "text file is busy",
139        "EAGAIN" => "resource temporarily unavailable",
140        "EBADF" => "bad file descriptor",
141        _ => "unknown error",
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148
149    #[test]
150    fn renders_like_node() {
151        let error = FsError::new("ENOENT", "open", Some("/x"));
152        assert_eq!(
153            error.to_string(),
154            "ENOENT: no such file or directory, open '/x'"
155        );
156        let error = FsError::new("EISDIR", "read", None);
157        assert_eq!(
158            error.to_string(),
159            "EISDIR: illegal operation on a directory, read"
160        );
161    }
162
163    #[test]
164    fn maps_io_kinds() {
165        let error = io::Error::from(io::ErrorKind::NotFound);
166        assert_eq!(FsError::from_io(&error, "stat", None).code, "ENOENT");
167    }
168}