1use std::{fmt, io};
2
3#[derive(Debug)]
5pub enum Error {
6 Server { code: String, message: String },
8 Protocol(String),
10 Io(io::Error),
12 Timeout(String),
17 TooLarge(String),
19 TooManyEntries(String),
22 Partial { written: usize, source: Box<Error> },
25}
26
27pub type Result<T> = std::result::Result<T, Error>;
28
29impl Error {
30 pub fn code(&self) -> Option<&str> {
33 match self {
34 Error::Server { code, .. } => Some(code),
35 Error::Timeout(_) => Some("DEADLINE_EXCEEDED"),
36 Error::Partial { source, .. } => source.code(),
37 _ => None,
38 }
39 }
40
41 pub(crate) fn dup(&self) -> Error {
43 match self {
44 Error::Server { code, message } => Error::Server {
45 code: code.clone(),
46 message: message.clone(),
47 },
48 Error::Protocol(m) => Error::Protocol(m.clone()),
49 Error::Io(e) => Error::Io(io::Error::new(e.kind(), e.to_string())),
50 Error::Timeout(m) => Error::Timeout(m.clone()),
51 Error::TooLarge(m) => Error::TooLarge(m.clone()),
52 Error::TooManyEntries(m) => Error::TooManyEntries(m.clone()),
53 Error::Partial { written, source } => Error::Partial {
54 written: *written,
55 source: Box::new(source.dup()),
56 },
57 }
58 }
59
60 pub(crate) fn server(code: &str, message: &str) -> Error {
61 Error::Server {
62 code: code.into(),
63 message: message.into(),
64 }
65 }
66}
67
68impl fmt::Display for Error {
69 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70 match self {
71 Error::Server { code, message } if message.is_empty() => write!(f, "{code}"),
72 Error::Server { code, message } => write!(f, "{code}: {message}"),
73 Error::Protocol(m) => write!(f, "protocol error: {m}"),
74 Error::Io(e) => write!(f, "io error: {e}"),
75 Error::Timeout(m) => write!(f, "DEADLINE_EXCEEDED: {m}"),
76 Error::TooLarge(m) => write!(f, "request too large: {m}"),
77 Error::TooManyEntries(m) => write!(f, "too many entries: {m}"),
78 Error::Partial { written, source } => write!(f, "partial write of {written} bytes: {source}"),
79 }
80 }
81}
82
83impl std::error::Error for Error {
84 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
85 match self {
86 Error::Io(e) => Some(e),
87 Error::Partial { source, .. } => Some(source.as_ref()),
88 _ => None,
89 }
90 }
91}
92
93impl From<io::Error> for Error {
94 fn from(e: io::Error) -> Error {
95 Error::Io(e)
96 }
97}
98
99impl From<Error> for io::Error {
100 fn from(e: Error) -> io::Error {
101 let kind = match &e {
102 Error::TooLarge(_) | Error::TooManyEntries(_) => io::ErrorKind::InvalidInput,
103 e => e.code().map_or(io::ErrorKind::Other, kind_for_code),
104 };
105 match e {
106 Error::Io(e) => e,
107 e => io::Error::new(kind, e),
108 }
109 }
110}
111
112pub(crate) fn kind_for_code(code: &str) -> io::ErrorKind {
115 use io::ErrorKind as K;
116 match code {
117 "ENOENT" => K::NotFound,
118 "EACCES" | "EPERM" => K::PermissionDenied,
119 "EEXIST" => K::AlreadyExists,
120 "EINVAL" => K::InvalidInput,
121 "ENOTDIR" => K::NotADirectory,
122 "EISDIR" => K::IsADirectory,
123 "ENOTEMPTY" => K::DirectoryNotEmpty,
124 "EROFS" => K::ReadOnlyFilesystem,
125 "ENOSPC" => K::StorageFull,
126 "EFBIG" => K::FileTooLarge,
127 "EXDEV" => K::CrossesDevices,
128 "EMLINK" => K::TooManyLinks,
129 "EBUSY" => K::ResourceBusy,
130 "ENAMETOOLONG" => K::InvalidFilename,
131 "EDQUOT" => K::QuotaExceeded,
132 "ESTALE" => K::StaleNetworkFileHandle,
133 "EAGAIN" => K::WouldBlock,
134 "UNSUPPORTED_OPERATION" | "UNSUPPORTED_VERSION" => K::Unsupported,
135 "DEADLINE_EXCEEDED" => K::TimedOut,
136 _ => K::Other,
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143 use io::ErrorKind as K;
144
145 #[test]
146 fn every_spec_code_maps() {
147 let want = [
148 ("ENOENT", K::NotFound),
149 ("EACCES", K::PermissionDenied),
150 ("EPERM", K::PermissionDenied),
151 ("EEXIST", K::AlreadyExists),
152 ("EINVAL", K::InvalidInput),
153 ("ENOTDIR", K::NotADirectory),
154 ("EISDIR", K::IsADirectory),
155 ("ENOTEMPTY", K::DirectoryNotEmpty),
156 ("EROFS", K::ReadOnlyFilesystem),
157 ("ENOSPC", K::StorageFull),
158 ("EFBIG", K::FileTooLarge),
159 ("EXDEV", K::CrossesDevices),
160 ("EMLINK", K::TooManyLinks),
161 ("EBUSY", K::ResourceBusy),
162 ("ENAMETOOLONG", K::InvalidFilename),
163 ("EDQUOT", K::QuotaExceeded),
164 ("ESTALE", K::StaleNetworkFileHandle),
165 ("EAGAIN", K::WouldBlock),
166 ("UNSUPPORTED_OPERATION", K::Unsupported),
167 ("UNSUPPORTED_VERSION", K::Unsupported),
168 ("DEADLINE_EXCEEDED", K::TimedOut),
169 ("EIO", K::Other),
170 ("EBADF", K::Other),
171 ("ELOOP", K::Other),
172 ("EUNKNOWN", K::Other),
173 ];
174 for (code, kind) in want {
175 let e: io::Error = Error::server(code, "m").into();
176 assert_eq!(e.kind(), kind, "{code}");
177 }
178 }
179
180 #[test]
181 fn code_survives_conversion() {
182 let e: io::Error = Error::server("ENOSPC", "disk full").into();
183 let inner = e.get_ref().unwrap().downcast_ref::<Error>().unwrap();
184 assert_eq!(inner.code(), Some("ENOSPC"));
185 }
186
187 #[test]
188 fn timeout_too_large_and_partial_convert() {
189 let e: io::Error = Error::Timeout("t".into()).into();
190 assert_eq!(e.kind(), K::TimedOut);
191 assert_eq!(Error::Timeout("t".into()).code(), Some("DEADLINE_EXCEEDED"));
192 let e: io::Error = Error::TooLarge("x".into()).into();
193 assert_eq!(e.kind(), K::InvalidInput);
194 let p = Error::Partial {
195 written: 5,
196 source: Box::new(Error::server("ENOSPC", "full")),
197 };
198 assert_eq!(p.code(), Some("ENOSPC"));
199 let e: io::Error = p.dup().into();
200 assert_eq!(e.kind(), K::StorageFull);
201 }
202
203 #[test]
204 fn non_server_errors_keep_other_kind() {
205 let e: io::Error = Error::Protocol("x".into()).into();
206 assert_eq!(e.kind(), K::Other);
207 }
208}