Skip to main content

maxminddb/
error.rs

1//! Error types for MaxMind DB operations.
2
3use std::fmt::Display;
4use std::io;
5
6use ipnetwork::IpNetworkError;
7use serde::de;
8use thiserror::Error;
9
10/// Error returned by MaxMind DB operations.
11#[derive(Error, Debug)]
12#[non_exhaustive]
13pub enum MaxMindDbError {
14    /// The database file is invalid or corrupted.
15    #[error("{}", format_invalid_database(.message, .offset))]
16    InvalidDatabase {
17        /// Description of what is invalid.
18        message: String,
19        /// Byte offset where the error was detected. Reader operations report
20        /// an absolute database-file offset; decoding a standalone section
21        /// reports an offset relative to that input.
22        offset: Option<usize>,
23    },
24
25    /// An I/O error occurred while reading the database.
26    #[error("i/o error: {0}")]
27    Io(
28        #[from]
29        #[source]
30        io::Error,
31    ),
32
33    /// Memory mapping failed.
34    #[cfg(feature = "mmap")]
35    #[error("memory map error: {0}")]
36    Mmap(#[source] io::Error),
37
38    /// Error decoding data from the database.
39    #[error(
40        "{}",
41        format_contextual_error("decoding error", .message, .offset, .path.as_deref())
42    )]
43    Decoding {
44        /// Description of the decoding error.
45        message: String,
46        /// Byte offset relative to the section being decoded: the data section
47        /// for records, or the metadata value after its marker for metadata.
48        offset: Option<usize>,
49        /// JSON-pointer-like path to the field (e.g., "/city/names/en").
50        path: Option<String>,
51    },
52
53    /// Decoding or verification stopped because it exceeded an expansion or
54    /// work safety limit.
55    ///
56    /// This does not necessarily mean that the database is structurally
57    /// invalid. Schema-specific limits reported by a custom Serde visitor use
58    /// [`MaxMindDbError::Decoding`] instead. Applications may choose a narrower
59    /// schema or reject the database as untrusted input.
60    #[error(
61        "{}",
62        format_contextual_error("resource limit exceeded", .message, .offset, .path.as_deref())
63    )]
64    ResourceLimit {
65        /// Description of the limit that was exceeded.
66        message: String,
67        /// Byte offset relative to the section being decoded: the data section
68        /// for records, or the metadata value after its marker for metadata.
69        offset: Option<usize>,
70        /// JSON-pointer-like path to the field (e.g., "/subdivisions").
71        path: Option<String>,
72    },
73
74    /// The provided network/CIDR is invalid.
75    #[error("invalid network: {0}")]
76    InvalidNetwork(
77        #[from]
78        #[source]
79        IpNetworkError,
80    ),
81
82    /// The provided input is invalid for this operation.
83    #[error("invalid input: {message}")]
84    InvalidInput {
85        /// Description of what is invalid about the input.
86        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    /// Creates an InvalidDatabase error with just a message.
113    pub fn invalid_database(message: impl Into<String>) -> Self {
114        MaxMindDbError::InvalidDatabase {
115            message: message.into(),
116            offset: None,
117        }
118    }
119
120    /// Creates an InvalidDatabase error with message and offset.
121    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    /// Creates a Decoding error with just a message.
129    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    /// Creates a Decoding error with message and offset.
138    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    /// Creates a Decoding error with message, offset, and path.
147    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    /// Creates a ResourceLimit error with a message and offset.
160    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    /// Translate a decoder-originated invalid-database offset from a section
169    /// into the containing database. Other error variants intentionally retain
170    /// their documented section-relative offsets.
171    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    /// Creates an InvalidInput error.
185    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        // Error without offset
218        assert_eq!(
219            format!(
220                "{}",
221                MaxMindDbError::invalid_database("something went wrong")
222            ),
223            "invalid database: something went wrong".to_owned(),
224        );
225        // Error with offset
226        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        // Decoding error without offset
249        assert_eq!(
250            format!("{}", MaxMindDbError::decoding("unexpected type")),
251            "decoding error: unexpected type".to_owned(),
252        );
253        // Decoding error with offset
254        assert_eq!(
255            format!("{}", MaxMindDbError::decoding_at("unexpected type", 100)),
256            "decoding error at offset 100: unexpected type".to_owned(),
257        );
258        // Decoding error with offset and path
259        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        // InvalidInput error
282        assert_eq!(
283            format!("{}", MaxMindDbError::invalid_input("bad address")),
284            "invalid input: bad address".to_owned(),
285        );
286    }
287}