1use std::fmt;
4use std::io;
5
6#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
8pub struct FsError {
9 pub code: String,
11 pub description: String,
13 pub syscall: String,
15 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 #[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 #[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#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
50#[error("spawn {program} {code}")]
51pub struct SpawnError {
52 pub program: String,
54 pub code: String,
56}
57
58impl SpawnError {
59 #[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
69fn 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
119fn 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}