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 in the database where the error was detected.
20        offset: Option<usize>,
21    },
22
23    /// An I/O error occurred while reading the database.
24    #[error("i/o error: {0}")]
25    Io(
26        #[from]
27        #[source]
28        io::Error,
29    ),
30
31    /// Memory mapping failed.
32    #[cfg(feature = "mmap")]
33    #[error("memory map error: {0}")]
34    Mmap(#[source] io::Error),
35
36    /// Error decoding data from the database.
37    #[error("{}", format_decoding_error(.message, .offset, .path.as_deref()))]
38    Decoding {
39        /// Description of the decoding error.
40        message: String,
41        /// Byte offset in the data section where the error occurred.
42        offset: Option<usize>,
43        /// JSON-pointer-like path to the field (e.g., "/city/names/en").
44        path: Option<String>,
45    },
46
47    /// The provided network/CIDR is invalid.
48    #[error("invalid network: {0}")]
49    InvalidNetwork(
50        #[from]
51        #[source]
52        IpNetworkError,
53    ),
54
55    /// The provided input is invalid for this operation.
56    #[error("invalid input: {message}")]
57    InvalidInput {
58        /// Description of what is invalid about the input.
59        message: String,
60    },
61}
62
63fn format_invalid_database(message: &str, offset: &Option<usize>) -> String {
64    match offset {
65        Some(off) => format!("invalid database at offset {off}: {message}"),
66        None => format!("invalid database: {message}"),
67    }
68}
69
70fn format_decoding_error(message: &str, offset: &Option<usize>, path: Option<&str>) -> String {
71    match (offset, path) {
72        (Some(off), Some(p)) => format!("decoding error at offset {off} (path: {p}): {message}"),
73        (Some(off), None) => format!("decoding error at offset {off}: {message}"),
74        (None, Some(p)) => format!("decoding error (path: {p}): {message}"),
75        (None, None) => format!("decoding error: {message}"),
76    }
77}
78
79impl MaxMindDbError {
80    /// Creates an InvalidDatabase error with just a message.
81    pub fn invalid_database(message: impl Into<String>) -> Self {
82        MaxMindDbError::InvalidDatabase {
83            message: message.into(),
84            offset: None,
85        }
86    }
87
88    /// Creates an InvalidDatabase error with message and offset.
89    pub fn invalid_database_at(message: impl Into<String>, offset: usize) -> Self {
90        MaxMindDbError::InvalidDatabase {
91            message: message.into(),
92            offset: Some(offset),
93        }
94    }
95
96    /// Creates a Decoding error with just a message.
97    pub fn decoding(message: impl Into<String>) -> Self {
98        MaxMindDbError::Decoding {
99            message: message.into(),
100            offset: None,
101            path: None,
102        }
103    }
104
105    /// Creates a Decoding error with message and offset.
106    pub fn decoding_at(message: impl Into<String>, offset: usize) -> Self {
107        MaxMindDbError::Decoding {
108            message: message.into(),
109            offset: Some(offset),
110            path: None,
111        }
112    }
113
114    /// Creates a Decoding error with message, offset, and path.
115    pub fn decoding_at_path(
116        message: impl Into<String>,
117        offset: usize,
118        path: impl Into<String>,
119    ) -> Self {
120        MaxMindDbError::Decoding {
121            message: message.into(),
122            offset: Some(offset),
123            path: Some(path.into()),
124        }
125    }
126
127    /// Creates an InvalidInput error.
128    pub fn invalid_input(message: impl Into<String>) -> Self {
129        MaxMindDbError::InvalidInput {
130            message: message.into(),
131        }
132    }
133}
134
135impl de::Error for MaxMindDbError {
136    fn custom<T: Display>(msg: T) -> Self {
137        MaxMindDbError::decoding(msg.to_string())
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144    use std::io::{Error, ErrorKind};
145
146    #[test]
147    fn test_error_display() {
148        // Error without offset
149        assert_eq!(
150            format!(
151                "{}",
152                MaxMindDbError::invalid_database("something went wrong")
153            ),
154            "invalid database: something went wrong".to_owned(),
155        );
156        // Error with offset
157        assert_eq!(
158            format!(
159                "{}",
160                MaxMindDbError::invalid_database_at("something went wrong", 42)
161            ),
162            "invalid database at offset 42: something went wrong".to_owned(),
163        );
164        let io_err = Error::new(ErrorKind::NotFound, "file not found");
165        assert_eq!(
166            format!("{}", MaxMindDbError::from(io_err)),
167            "i/o error: file not found".to_owned(),
168        );
169
170        #[cfg(feature = "mmap")]
171        {
172            let mmap_io_err = Error::new(ErrorKind::PermissionDenied, "mmap failed");
173            assert_eq!(
174                format!("{}", MaxMindDbError::Mmap(mmap_io_err)),
175                "memory map error: mmap failed".to_owned(),
176            );
177        }
178
179        // Decoding error without offset
180        assert_eq!(
181            format!("{}", MaxMindDbError::decoding("unexpected type")),
182            "decoding error: unexpected type".to_owned(),
183        );
184        // Decoding error with offset
185        assert_eq!(
186            format!("{}", MaxMindDbError::decoding_at("unexpected type", 100)),
187            "decoding error at offset 100: unexpected type".to_owned(),
188        );
189        // Decoding error with offset and path
190        assert_eq!(
191            format!(
192                "{}",
193                MaxMindDbError::decoding_at_path("unexpected type", 100, "/city/names/en")
194            ),
195            "decoding error at offset 100 (path: /city/names/en): unexpected type".to_owned(),
196        );
197
198        let net_err = IpNetworkError::InvalidPrefix;
199        assert_eq!(
200            format!("{}", MaxMindDbError::from(net_err)),
201            "invalid network: invalid prefix".to_owned(),
202        );
203
204        // InvalidInput error
205        assert_eq!(
206            format!("{}", MaxMindDbError::invalid_input("bad address")),
207            "invalid input: bad address".to_owned(),
208        );
209    }
210}