1use std::fmt::Display;
4use std::io;
5
6use ipnetwork::IpNetworkError;
7use serde::de;
8use thiserror::Error;
9
10#[derive(Error, Debug)]
12#[non_exhaustive]
13pub enum MaxMindDbError {
14 #[error("{}", format_invalid_database(.message, .offset))]
16 InvalidDatabase {
17 message: String,
19 offset: Option<usize>,
23 },
24
25 #[error("i/o error: {0}")]
27 Io(
28 #[from]
29 #[source]
30 io::Error,
31 ),
32
33 #[cfg(feature = "mmap")]
35 #[error("memory map error: {0}")]
36 Mmap(#[source] io::Error),
37
38 #[error(
40 "{}",
41 format_contextual_error("decoding error", .message, .offset, .path.as_deref())
42 )]
43 Decoding {
44 message: String,
46 offset: Option<usize>,
49 path: Option<String>,
51 },
52
53 #[error(
61 "{}",
62 format_contextual_error("resource limit exceeded", .message, .offset, .path.as_deref())
63 )]
64 ResourceLimit {
65 message: String,
67 offset: Option<usize>,
70 path: Option<String>,
72 },
73
74 #[error("invalid network: {0}")]
76 InvalidNetwork(
77 #[from]
78 #[source]
79 IpNetworkError,
80 ),
81
82 #[error("invalid input: {message}")]
84 InvalidInput {
85 message: String,
87 },
88}
89
90fn format_invalid_database(message: &str, offset: &Option<usize>) -> String {
91 match offset {
92 Some(off) => format!("invalid database at offset {off}: {message}"),
93 None => format!("invalid database: {message}"),
94 }
95}
96
97fn format_contextual_error(
98 prefix: &str,
99 message: &str,
100 offset: &Option<usize>,
101 path: Option<&str>,
102) -> String {
103 match (offset, path) {
104 (Some(off), Some(p)) => format!("{prefix} at offset {off} (path: {p}): {message}"),
105 (Some(off), None) => format!("{prefix} at offset {off}: {message}"),
106 (None, Some(p)) => format!("{prefix} (path: {p}): {message}"),
107 (None, None) => format!("{prefix}: {message}"),
108 }
109}
110
111impl MaxMindDbError {
112 pub fn invalid_database(message: impl Into<String>) -> Self {
114 MaxMindDbError::InvalidDatabase {
115 message: message.into(),
116 offset: None,
117 }
118 }
119
120 pub fn invalid_database_at(message: impl Into<String>, offset: usize) -> Self {
122 MaxMindDbError::InvalidDatabase {
123 message: message.into(),
124 offset: Some(offset),
125 }
126 }
127
128 pub fn decoding(message: impl Into<String>) -> Self {
130 MaxMindDbError::Decoding {
131 message: message.into(),
132 offset: None,
133 path: None,
134 }
135 }
136
137 pub fn decoding_at(message: impl Into<String>, offset: usize) -> Self {
139 MaxMindDbError::Decoding {
140 message: message.into(),
141 offset: Some(offset),
142 path: None,
143 }
144 }
145
146 pub fn decoding_at_path(
148 message: impl Into<String>,
149 offset: usize,
150 path: impl Into<String>,
151 ) -> Self {
152 MaxMindDbError::Decoding {
153 message: message.into(),
154 offset: Some(offset),
155 path: Some(path.into()),
156 }
157 }
158
159 pub fn resource_limit_at(message: impl Into<String>, offset: usize) -> Self {
161 MaxMindDbError::ResourceLimit {
162 message: message.into(),
163 offset: Some(offset),
164 path: None,
165 }
166 }
167
168 pub(crate) fn with_invalid_database_offset_base(self, base: usize) -> Self {
172 match self {
173 MaxMindDbError::InvalidDatabase {
174 message,
175 offset: Some(offset),
176 } => MaxMindDbError::InvalidDatabase {
177 message,
178 offset: offset.checked_add(base),
179 },
180 _ => self,
181 }
182 }
183
184 pub fn invalid_input(message: impl Into<String>) -> Self {
186 MaxMindDbError::InvalidInput {
187 message: message.into(),
188 }
189 }
190}
191
192impl de::Error for MaxMindDbError {
193 fn custom<T: Display>(msg: T) -> Self {
194 MaxMindDbError::decoding(msg.to_string())
195 }
196}
197
198impl From<Box<MaxMindDbError>> for MaxMindDbError {
199 fn from(error: Box<MaxMindDbError>) -> Self {
200 *error
201 }
202}
203
204impl de::Error for Box<MaxMindDbError> {
205 fn custom<T: Display>(msg: T) -> Self {
206 Box::new(MaxMindDbError::decoding(msg.to_string()))
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213 use std::io::{Error, ErrorKind};
214
215 #[test]
216 fn test_error_display() {
217 assert_eq!(
219 format!(
220 "{}",
221 MaxMindDbError::invalid_database("something went wrong")
222 ),
223 "invalid database: something went wrong".to_owned(),
224 );
225 assert_eq!(
227 format!(
228 "{}",
229 MaxMindDbError::invalid_database_at("something went wrong", 42)
230 ),
231 "invalid database at offset 42: something went wrong".to_owned(),
232 );
233 let io_err = Error::new(ErrorKind::NotFound, "file not found");
234 assert_eq!(
235 format!("{}", MaxMindDbError::from(io_err)),
236 "i/o error: file not found".to_owned(),
237 );
238
239 #[cfg(feature = "mmap")]
240 {
241 let mmap_io_err = Error::new(ErrorKind::PermissionDenied, "mmap failed");
242 assert_eq!(
243 format!("{}", MaxMindDbError::Mmap(mmap_io_err)),
244 "memory map error: mmap failed".to_owned(),
245 );
246 }
247
248 assert_eq!(
250 format!("{}", MaxMindDbError::decoding("unexpected type")),
251 "decoding error: unexpected type".to_owned(),
252 );
253 assert_eq!(
255 format!("{}", MaxMindDbError::decoding_at("unexpected type", 100)),
256 "decoding error at offset 100: unexpected type".to_owned(),
257 );
258 assert_eq!(
260 format!(
261 "{}",
262 MaxMindDbError::decoding_at_path("unexpected type", 100, "/city/names/en")
263 ),
264 "decoding error at offset 100 (path: /city/names/en): unexpected type".to_owned(),
265 );
266
267 assert_eq!(
268 format!(
269 "{}",
270 MaxMindDbError::resource_limit_at("too many values", 100)
271 ),
272 "resource limit exceeded at offset 100: too many values".to_owned(),
273 );
274
275 let net_err = IpNetworkError::InvalidPrefix;
276 assert_eq!(
277 format!("{}", MaxMindDbError::from(net_err)),
278 "invalid network: invalid prefix".to_owned(),
279 );
280
281 assert_eq!(
283 format!("{}", MaxMindDbError::invalid_input("bad address")),
284 "invalid input: bad address".to_owned(),
285 );
286 }
287}