From ae20d19d7af0f30d12bd05258f73e35b2e26cac2 Mon Sep 17 00:00:00 2001
From: Gregory Oschwald <goschwald@maxmind.com>
Date: Tue, 25 Mar 2025 11:01:44 -0700
Subject: [PATCH 1/4] Fix a bug in the bounds checking

when resolving a data pointer.
---
 CHANGELOG.md         | 6 ++++++
 src/maxminddb/lib.rs | 8 ++++----
 2 files changed, 10 insertions(+), 4 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 04ff5b9f..b1fcd9b9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
 # Change Log #
 
+## 0.26.0
+
+* Fixed an internal bounds checking error when resolving data pointers.
+  The previous logic could cause a panic on a corrupt database.
+
+
 ## 0.25.0 - 2025-02-16
 
 * Serde will now skip serialization of the GeoIP2 struct fields
diff --git a/src/maxminddb/lib.rs b/src/maxminddb/lib.rs
index e5bf6f40..22ed284a 100644
--- a/src/maxminddb/lib.rs
+++ b/src/maxminddb/lib.rs
@@ -492,10 +492,10 @@ impl<'de, S: AsRef<[u8]>> Reader<S> {
     fn resolve_data_pointer(&self, pointer: usize) -> Result<usize, MaxMindDBError> {
         let resolved = pointer - (self.metadata.node_count as usize) - 16;
 
-        if resolved > self.buf.as_ref().len() {
-            return Err(MaxMindDBError::InvalidDatabaseError(
-                "the MaxMind DB file's search tree \
-                 is corrupt"
+        // Check bounds using pointer_base which marks the start of the data section
+        if resolved >= (self.buf.as_ref().len() - self.pointer_base) {
+             return Err(MaxMindDBError::InvalidDatabaseError(
+                 "the MaxMind DB file's data pointer resolves to an invalid location"
                     .to_owned(),
             ));
         }

From da7941078617e8e77eda186652a22f9c38fe0ed9 Mon Sep 17 00:00:00 2001
From: Gregory Oschwald <goschwald@maxmind.com>
Date: Tue, 25 Mar 2025 11:07:12 -0700
Subject: [PATCH 2/4] Return an Option rather than AddressNotFoundError

when an address is not in the database. This seems more idiomatic given
that it is an expected situation. This will hopefully make the error
handling simpler.

Closes #83.
---
 CHANGELOG.md                 |   8 ++
 examples/lookup.rs           |   2 +-
 src/maxminddb/lib.rs         | 164 +++++++++++++++++-----
 src/maxminddb/reader_test.rs | 256 ++++++++++++++++++++++-------------
 4 files changed, 296 insertions(+), 134 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index b1fcd9b9..3d7379b0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,14 @@
 
 ## 0.26.0
 
+* **BREAKING CHANGE:** The `lookup` and `lookup_prefix` methods now return
+  `Ok(None)` or `Ok((None, prefix_len))` respectively when an IP address is
+  valid but not found in the database (or has no associated data record),
+  instead of returning an `Err(MaxMindDBError::AddressNotFoundError)`.
+  Code previously matching on `AddressNotFoundError` must be updated to
+  handle the `Ok(None)` / `Ok((None, prefix_len))` variants.
+* `lookup_prefix` now returns the prefix length of the entry even when the
+  value is not found.
 * Fixed an internal bounds checking error when resolving data pointers.
   The previous logic could cause a panic on a corrupt database.
 
diff --git a/examples/lookup.rs b/examples/lookup.rs
index 647aca84..6aa27c57 100644
--- a/examples/lookup.rs
+++ b/examples/lookup.rs
@@ -14,7 +14,7 @@ fn main() -> Result<(), String> {
         .ok_or("Second argument must be the IP address, like 128.101.101.101")?
         .parse()
         .unwrap();
-    let city: geoip2::City = reader.lookup(ip).unwrap();
+    let city: Option<geoip2::City> = reader.lookup(ip).unwrap();
     println!("{city:#?}");
     Ok(())
 }
diff --git a/src/maxminddb/lib.rs b/src/maxminddb/lib.rs
index 22ed284a..b8c84a54 100644
--- a/src/maxminddb/lib.rs
+++ b/src/maxminddb/lib.rs
@@ -23,7 +23,6 @@ compile_error!("features `simdutf8` and `unsafe-str-decode` are mutually exclusi
 
 #[derive(Debug, PartialEq, Eq)]
 pub enum MaxMindDBError {
-    AddressNotFoundError(String),
     InvalidDatabaseError(String),
     IoError(String),
     MapError(String),
@@ -41,9 +40,6 @@ impl From<io::Error> for MaxMindDBError {
 impl Display for MaxMindDBError {
     fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), fmt::Error> {
         match self {
-            MaxMindDBError::AddressNotFoundError(msg) => {
-                write!(fmt, "AddressNotFoundError: {msg}")?
-            }
             MaxMindDBError::InvalidDatabaseError(msg) => {
                 write!(fmt, "InvalidDatabaseError: {msg}")?
             }
@@ -231,7 +227,10 @@ impl<'de> Reader<Mmap> {
     /// # Example
     ///
     /// ```
+    /// # #[cfg(feature = "mmap")]
+    /// # {
     /// let reader = maxminddb::Reader::open_mmap("test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
+    /// # }
     /// ```
     pub fn open_mmap<P: AsRef<Path>>(database: P) -> Result<Reader<Mmap>, MaxMindDBError> {
         let file_read = File::open(database)?;
@@ -286,59 +285,93 @@ impl<'de, S: AsRef<[u8]>> Reader<S> {
         Ok(reader)
     }
 
-    /// Lookup the socket address in the opened MaxMind DB
+    /// Lookup the socket address in the opened MaxMind DB.
+    /// Returns `Ok(None)` if the address is not found in the database.
     ///
     /// Example:
     ///
     /// ```
-    /// use maxminddb::geoip2;
-    /// use std::net::IpAddr;
-    /// use std::str::FromStr;
-    ///
-    /// let reader = maxminddb::Reader::open_readfile("test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
+    /// # use maxminddb::geoip2;
+    /// # use std::net::IpAddr;
+    /// # use std::str::FromStr;
+    /// # fn main() -> Result<(), maxminddb::MaxMindDBError> {
+    /// let reader = maxminddb::Reader::open_readfile("test-data/test-data/GeoIP2-City-Test.mmdb")?;
     ///
     /// let ip: IpAddr = FromStr::from_str("89.160.20.128").unwrap();
-    /// let city: geoip2::City = reader.lookup(ip).unwrap();
-    /// print!("{:?}", city);
+    /// if let Some(city) = reader.lookup::<geoip2::City>(ip)? {
+    ///     println!("{:?}", city);
+    /// } else {
+    ///     println!("Address not found");
+    /// }
+    /// # Ok(())
+    /// # }
     /// ```
-    pub fn lookup<T>(&'de self, address: IpAddr) -> Result<T, MaxMindDBError>
+    pub fn lookup<T>(&'de self, address: IpAddr) -> Result<Option<T>, MaxMindDBError>
     where
         T: Deserialize<'de>,
     {
-        self.lookup_prefix(address).map(|(v, _)| v)
+        self.lookup_prefix(address)
+            .map(|(option_value, _prefix_len)| option_value)
     }
 
-    /// Lookup the socket address in the opened MaxMind DB
+    /// Lookup the socket address in the opened MaxMind DB, returning the found value (if any)
+    /// and the prefix length of the network associated with the lookup.
+    ///
+    /// Returns `Ok((None, prefix_len))` if the address is found in the tree but has no data record.
+    /// Returns `Err(...)` for database errors (IO, corruption, decoding).
     ///
     /// Example:
     ///
     /// ```
-    /// use maxminddb::geoip2;
-    /// use std::net::IpAddr;
-    /// use std::str::FromStr;
+    /// # use maxminddb::geoip2;
+    /// # use std::net::IpAddr;
+    /// # use std::str::FromStr;
+    /// # fn main() -> Result<(), maxminddb::MaxMindDBError> {
+    /// let reader = maxminddb::Reader::open_readfile("test-data/test-data/GeoIP2-City-Test.mmdb")?;
     ///
-    /// let reader = maxminddb::Reader::open_readfile("test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
+    /// let ip: IpAddr = "89.160.20.128".parse().unwrap(); // Known IP
+    /// let ip_unknown: IpAddr = "10.0.0.1".parse().unwrap(); // Unknown IP
+    ///
+    /// let (city_option, prefix_len) = reader.lookup_prefix::<geoip2::City>(ip)?;
+    /// if let Some(city) = city_option {
+    ///     println!("Found {:?} at prefix length {}", city.city.unwrap().names.unwrap().get("en").unwrap(), prefix_len);
+    /// } else {
+    ///     // This case is less likely with lookup_prefix if the IP resolves in the tree
+    ///     println!("IP found in tree but no data (prefix_len: {})", prefix_len);
+    /// }
     ///
-    /// let ip: IpAddr = "89.160.20.128".parse().unwrap();
-    /// let (city, prefix_len) = reader.lookup_prefix::<geoip2::City>(ip).unwrap();
-    /// print!("{:?}, prefix length: {}", city, prefix_len);
+    /// let (city_option_unknown, prefix_len_unknown) = reader.lookup_prefix::<geoip2::City>(ip_unknown)?;
+    /// assert!(city_option_unknown.is_none());
+    /// println!("Unknown IP resolved to prefix_len: {}", prefix_len_unknown);
+    /// # Ok(())
+    /// # }
     /// ```
-    pub fn lookup_prefix<T>(&'de self, address: IpAddr) -> Result<(T, usize), MaxMindDBError>
+    pub fn lookup_prefix<T>(
+        &'de self,
+        address: IpAddr,
+    ) -> Result<(Option<T>, usize), MaxMindDBError>
     where
         T: Deserialize<'de>,
     {
         let ip_int = IpInt::new(address);
+        // find_address_in_tree returns Result<(usize, usize), MaxMindDBError> -> (pointer, prefix_len)
         let (pointer, prefix_len) = self.find_address_in_tree(&ip_int)?;
+
         if pointer == 0 {
-            return Err(MaxMindDBError::AddressNotFoundError(
-                "Address not found in database".to_owned(),
-            ));
+            // If pointer is 0, it signifies no data record was associated during tree traversal.
+            // Return None for the data, but include the calculated prefix_len.
+            return Ok((None, prefix_len));
         }
 
+        // If pointer > 0, attempt to resolve and decode data
         let rec = self.resolve_data_pointer(pointer)?;
         let mut decoder = decoder::Decoder::new(&self.buf.as_ref()[self.pointer_base..], rec);
 
-        T::deserialize(&mut decoder).map(|v| (v, prefix_len))
+        // Attempt to deserialize. If successful, wrap in Some. If error, propagate.
+        match T::deserialize(&mut decoder) {
+            Ok(value) => Ok((Some(value), prefix_len)),
+            Err(e) => Err(e),
+        }
     }
 
     /// Iterate over blocks of IP networks in the opened MaxMind DB
@@ -423,6 +456,8 @@ impl<'de, S: AsRef<[u8]>> Reader<S> {
             node = self.read_node(node, bit as usize)?;
         }
         match node_count {
+            // If node == node_count, it means we hit the placeholder "empty" node
+            // return 0 as the pointer value to signify "not found".
             n if n == node => Ok((0, prefix_len)),
             n if node > n => Ok((node, prefix_len)),
             _ => Err(MaxMindDBError::InvalidDatabaseError(
@@ -494,9 +529,8 @@ impl<'de, S: AsRef<[u8]>> Reader<S> {
 
         // Check bounds using pointer_base which marks the start of the data section
         if resolved >= (self.buf.as_ref().len() - self.pointer_base) {
-             return Err(MaxMindDBError::InvalidDatabaseError(
-                 "the MaxMind DB file's data pointer resolves to an invalid location"
-                    .to_owned(),
+            return Err(MaxMindDBError::InvalidDatabaseError(
+                "the MaxMind DB file's data pointer resolves to an invalid location".to_owned(),
             ));
         }
 
@@ -548,13 +582,6 @@ mod tests {
 
     #[test]
     fn test_error_display() {
-        assert_eq!(
-            format!(
-                "{}",
-                MaxMindDBError::AddressNotFoundError("something went wrong".to_owned())
-            ),
-            "AddressNotFoundError: something went wrong".to_owned(),
-        );
         assert_eq!(
             format!(
                 "{}",
@@ -583,5 +610,68 @@ mod tests {
             ),
             "DecodingError: something went wrong".to_owned(),
         );
+        assert_eq!(
+            format!(
+                "{}",
+                MaxMindDBError::InvalidNetworkError("something went wrong".to_owned())
+            ),
+            "InvalidNetworkError: something went wrong".to_owned(),
+        );
+    }
+
+    #[test]
+    fn test_lookup_returns_none_for_unknown_address() {
+        use super::Reader;
+        use crate::geoip2;
+        use std::net::IpAddr;
+        use std::str::FromStr;
+
+        let reader = Reader::open_readfile("test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
+        let ip: IpAddr = FromStr::from_str("10.0.0.1").unwrap();
+
+        let result_lookup = reader.lookup::<geoip2::City>(ip);
+        assert!(
+            matches!(result_lookup, Ok(None)),
+            "lookup should return Ok(None) for unknown IP"
+        );
+
+        let result_lookup_prefix = reader.lookup_prefix::<geoip2::City>(ip);
+        assert!(
+            matches!(result_lookup_prefix, Ok((None, 8))),
+            "lookup_prefix should return Ok(None) for unknown IP"
+        );
+    }
+
+    #[test]
+    fn test_lookup_returns_some_for_known_address() {
+        use super::Reader;
+        use crate::geoip2;
+        use std::net::IpAddr;
+        use std::str::FromStr;
+
+        let reader = Reader::open_readfile("test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
+        let ip: IpAddr = FromStr::from_str("89.160.20.128").unwrap();
+
+        let result_lookup = reader.lookup::<geoip2::City>(ip);
+        assert!(
+            matches!(result_lookup, Ok(Some(_))),
+            "lookup should return Ok(Some(_)) for known IP"
+        );
+        assert!(
+            result_lookup.unwrap().unwrap().city.is_some(),
+            "Expected city data"
+        );
+
+        let result_lookup_prefix = reader.lookup_prefix::<geoip2::City>(ip);
+        assert!(
+            matches!(result_lookup_prefix, Ok((Some(_), _))),
+            "lookup_prefix should return Ok(Some(_)) for known IP"
+        );
+        let (city_data, prefix_len) = result_lookup_prefix.unwrap();
+        assert!(
+            city_data.unwrap().city.is_some(),
+            "Expected city data from prefix lookup"
+        );
+        assert_eq!(prefix_len, 25, "Expected valid prefix length");
     }
 }
diff --git a/src/maxminddb/reader_test.rs b/src/maxminddb/reader_test.rs
index 38e73932..dbaa9ad6 100644
--- a/src/maxminddb/reader_test.rs
+++ b/src/maxminddb/reader_test.rs
@@ -1,10 +1,12 @@
 use std::net::IpAddr;
 use std::str::FromStr;
 
+use ipnetwork::IpNetwork;
 use serde::Deserialize;
 use serde_json::json;
 
-use super::{MaxMindDBError, Reader};
+use crate::geoip2;
+use crate::{MaxMindDBError, Reader, Within};
 
 #[allow(clippy::float_cmp)]
 #[test]
@@ -46,7 +48,7 @@ fn test_decoder() {
     }
     let r = r.unwrap();
     let ip: IpAddr = FromStr::from_str("1.1.1.0").unwrap();
-    let result: TestType = r.lookup(ip).unwrap();
+    let result: TestType = r.lookup(ip).unwrap().unwrap();
 
     assert_eq!(result.array, vec![1_u32, 2_u32, 3_u32]);
     assert!(result.boolean);
@@ -87,6 +89,7 @@ fn test_pointers_in_metadata() {
     if let Err(err) = r {
         panic!("error opening mmdb: {err:?}");
     }
+    r.unwrap();
 }
 
 #[test]
@@ -101,11 +104,12 @@ fn test_broken_database() {
     #[derive(Deserialize, Debug)]
     struct TestType {}
     match r.lookup::<TestType>(ip) {
-        Err(e) => assert_eq!(
+        Err(e) => assert!(matches!(
             e,
-            MaxMindDBError::InvalidDatabaseError("double of size 2".to_string())
-        ),
-        Ok(_) => panic!("Error expected"),
+            MaxMindDBError::InvalidDatabaseError(_) // Check variant, message might vary slightly
+        )),
+        Ok(Some(_)) => panic!("Unexpected success with broken data"),
+        Ok(None) => panic!("Got None, expected InvalidDatabaseError"),
     }
 }
 
@@ -116,11 +120,7 @@ fn test_missing_database() {
     let r = Reader::open_readfile("file-does-not-exist.mmdb");
     match r {
         Ok(_) => panic!("Received Reader when opening non-existent file"),
-        Err(e) => assert!(
-            e == MaxMindDBError::IoError(
-                "The system cannot find the file specified. (os error 2)".to_string()
-            ) || e == MaxMindDBError::IoError("No such file or directory (os error 2)".to_string())
-        ),
+        Err(e) => assert!(matches!(e, MaxMindDBError::IoError(_))), // Specific message might vary by OS/locale
     }
 }
 
@@ -134,9 +134,7 @@ fn test_non_database() {
         Err(e) => assert_eq!(
             e,
             MaxMindDBError::InvalidDatabaseError(
-                "Could not find MaxMind DB metadata \
-                 in file."
-                    .to_string(),
+                "Could not find MaxMind DB metadata in file.".to_owned(),
             )
         ),
     }
@@ -182,6 +180,7 @@ fn test_reader_readfile() {
 #[test]
 #[cfg(feature = "mmap")]
 fn test_reader_mmap() {
+    use crate::Mmap;
     let _ = env_logger::try_init();
 
     let sizes = [24usize, 28, 32];
@@ -202,7 +201,6 @@ fn test_reader_mmap() {
 
 #[test]
 fn test_lookup_city() {
-    use super::geoip2::City;
     let _ = env_logger::try_init();
 
     let filename = "test-data/test-data/GeoIP2-City-Test.mmdb";
@@ -210,7 +208,7 @@ fn test_lookup_city() {
     let reader = Reader::open_readfile(filename).unwrap();
 
     let ip: IpAddr = FromStr::from_str("89.160.20.112").unwrap();
-    let city: City = reader.lookup(ip).unwrap();
+    let city: geoip2::City = reader.lookup(ip).unwrap().unwrap();
 
     let iso_code = city.country.and_then(|cy| cy.iso_code);
 
@@ -219,7 +217,6 @@ fn test_lookup_city() {
 
 #[test]
 fn test_lookup_country() {
-    use super::geoip2::Country;
     let _ = env_logger::try_init();
 
     let filename = "test-data/test-data/GeoIP2-Country-Test.mmdb";
@@ -227,7 +224,7 @@ fn test_lookup_country() {
     let reader = Reader::open_readfile(filename).unwrap();
 
     let ip: IpAddr = FromStr::from_str("89.160.20.112").unwrap();
-    let country: Country = reader.lookup(ip).unwrap();
+    let country: geoip2::Country = reader.lookup(ip).unwrap().unwrap();
     let country = country.country.unwrap();
 
     assert_eq!(country.iso_code, Some("SE"));
@@ -236,7 +233,6 @@ fn test_lookup_country() {
 
 #[test]
 fn test_lookup_connection_type() {
-    use super::geoip2::ConnectionType;
     let _ = env_logger::try_init();
 
     let filename = "test-data/test-data/GeoIP2-Connection-Type-Test.mmdb";
@@ -244,14 +240,13 @@ fn test_lookup_connection_type() {
     let reader = Reader::open_readfile(filename).unwrap();
 
     let ip: IpAddr = FromStr::from_str("96.1.20.112").unwrap();
-    let connection_type: ConnectionType = reader.lookup(ip).unwrap();
+    let connection_type: geoip2::ConnectionType = reader.lookup(ip).unwrap().unwrap();
 
     assert_eq!(connection_type.connection_type, Some("Cable/DSL"));
 }
 
 #[test]
 fn test_lookup_annonymous_ip() {
-    use super::geoip2::AnonymousIp;
     let _ = env_logger::try_init();
 
     let filename = "test-data/test-data/GeoIP2-Anonymous-IP-Test.mmdb";
@@ -259,7 +254,7 @@ fn test_lookup_annonymous_ip() {
     let reader = Reader::open_readfile(filename).unwrap();
 
     let ip: IpAddr = FromStr::from_str("81.2.69.123").unwrap();
-    let anonymous_ip: AnonymousIp = reader.lookup(ip).unwrap();
+    let anonymous_ip: geoip2::AnonymousIp = reader.lookup(ip).unwrap().unwrap();
 
     assert_eq!(anonymous_ip.is_anonymous, Some(true));
     assert_eq!(anonymous_ip.is_public_proxy, Some(true));
@@ -270,7 +265,6 @@ fn test_lookup_annonymous_ip() {
 
 #[test]
 fn test_lookup_density_income() {
-    use super::geoip2::DensityIncome;
     let _ = env_logger::try_init();
 
     let filename = "test-data/test-data/GeoIP2-DensityIncome-Test.mmdb";
@@ -278,7 +272,7 @@ fn test_lookup_density_income() {
     let reader = Reader::open_readfile(filename).unwrap();
 
     let ip: IpAddr = FromStr::from_str("5.83.124.123").unwrap();
-    let density_income: DensityIncome = reader.lookup(ip).unwrap();
+    let density_income: geoip2::DensityIncome = reader.lookup(ip).unwrap().unwrap();
 
     assert_eq!(density_income.average_income, Some(32323));
     assert_eq!(density_income.population_density, Some(1232))
@@ -286,7 +280,6 @@ fn test_lookup_density_income() {
 
 #[test]
 fn test_lookup_domain() {
-    use super::geoip2::Domain;
     let _ = env_logger::try_init();
 
     let filename = "test-data/test-data/GeoIP2-Domain-Test.mmdb";
@@ -294,14 +287,13 @@ fn test_lookup_domain() {
     let reader = Reader::open_readfile(filename).unwrap();
 
     let ip: IpAddr = FromStr::from_str("66.92.80.123").unwrap();
-    let domain: Domain = reader.lookup(ip).unwrap();
+    let domain: geoip2::Domain = reader.lookup(ip).unwrap().unwrap();
 
     assert_eq!(domain.domain, Some("speakeasy.net"));
 }
 
 #[test]
 fn test_lookup_isp() {
-    use super::geoip2::Isp;
     let _ = env_logger::try_init();
 
     let filename = "test-data/test-data/GeoIP2-ISP-Test.mmdb";
@@ -309,7 +301,7 @@ fn test_lookup_isp() {
     let reader = Reader::open_readfile(filename).unwrap();
 
     let ip: IpAddr = FromStr::from_str("12.87.118.123").unwrap();
-    let isp: Isp = reader.lookup(ip).unwrap();
+    let isp: geoip2::Isp = reader.lookup(ip).unwrap().unwrap();
 
     assert_eq!(isp.autonomous_system_number, Some(7018));
     assert_eq!(isp.isp, Some("AT&T Services"));
@@ -318,15 +310,14 @@ fn test_lookup_isp() {
 
 #[test]
 fn test_lookup_asn() {
-    use super::geoip2::Asn;
     let _ = env_logger::try_init();
 
-    let filename = "test-data/test-data/GeoIP2-ISP-Test.mmdb";
+    let filename = "test-data/test-data/GeoLite2-ASN-Test.mmdb";
 
     let reader = Reader::open_readfile(filename).unwrap();
 
     let ip: IpAddr = FromStr::from_str("1.128.0.123").unwrap();
-    let asn: Asn = reader.lookup(ip).unwrap();
+    let asn: geoip2::Asn = reader.lookup(ip).unwrap().unwrap();
 
     assert_eq!(asn.autonomous_system_number, Some(1221));
     assert_eq!(asn.autonomous_system_organization, Some("Telstra Pty Ltd"));
@@ -334,83 +325,128 @@ fn test_lookup_asn() {
 
 #[test]
 fn test_lookup_prefix() {
-    use super::geoip2::City;
     let _ = env_logger::try_init();
-
-    let filename = "test-data/test-data/GeoIP2-ISP-Test.mmdb";
-
+    let filename = "test-data/test-data/GeoIP2-City-Test.mmdb";
     let reader = Reader::open_readfile(filename).unwrap();
 
-    // IPv4
+    // --- IPv4 Check (Known) ---
     let ip: IpAddr = "89.160.20.128".parse().unwrap();
-    let (_, prefix_len) = reader.lookup_prefix::<City>(ip).unwrap();
-
-    assert_eq!(prefix_len, 25); // "::89.160.20.128/121"
-
-    // Last host
-    let ip: IpAddr = "89.160.20.254".parse().unwrap();
-    let (_, last_prefix_len) = reader.lookup_prefix::<City>(ip).unwrap();
-
-    assert_eq!(prefix_len, last_prefix_len);
-
-    // IPv6
-    let ip: IpAddr = "2c0f:ff00::1".parse().unwrap();
-    let (_, prefix_len) = reader.lookup_prefix::<City>(ip).unwrap();
+    let result_v4 = reader.lookup_prefix::<geoip2::City>(ip);
+    assert!(result_v4.is_ok());
+    let (city_opt_v4, prefix_len_v4) = result_v4.unwrap();
+    assert!(city_opt_v4.is_some(), "Expected Some(City) for known IPv4");
+    assert_eq!(prefix_len_v4, 25);
+    assert!(city_opt_v4.unwrap().country.is_some());
+
+    // --- IPv4 Check (Last Host, Known) ---
+    let ip_last: IpAddr = "89.160.20.254".parse().unwrap();
+    let (city_opt_last, last_prefix_len) = reader.lookup_prefix::<geoip2::City>(ip_last).unwrap();
+    assert!(city_opt_last.is_some(), "Expected Some(City) for last host");
+    assert_eq!(last_prefix_len, 25); // Should be same network
+
+    // --- IPv6 Check (Not Found in Data) ---
+    // This IP might resolve to a node in the tree, but that node might not point to data.
+    let ip_v6_not_found: IpAddr = "2c0f:ff00::1".parse().unwrap();
+    let result_not_found = reader.lookup_prefix::<geoip2::City>(ip_v6_not_found);
+    assert!(result_not_found.is_ok());
+    let (city_opt_nf, prefix_len_nf) = result_not_found.unwrap();
+    assert!(
+        city_opt_nf.is_none(),
+        "Expected None data for non-existent IP 2c0f:ff00::1"
+    );
+    assert_eq!(
+        prefix_len_nf, 6,
+        "Expected valid prefix length for not-found IPv6"
+    );
 
-    assert_eq!(prefix_len, 26); // "2c0f:ff00::/26"
+    // --- IPv6 Check (Known Data) ---
+    let ip_v6_known: IpAddr = "2001:218:85a3:0:0:8a2e:370:7334".parse().unwrap();
+    let result_known_v6 = reader.lookup_prefix::<geoip2::City>(ip_v6_known);
+    assert!(result_known_v6.is_ok());
+    let (city_opt_v6, prefix_len_v6_known) = result_known_v6.unwrap();
+    assert!(city_opt_v6.is_some(), "Expected Some(City) for known IPv6");
+    assert_eq!(
+        prefix_len_v6_known, 32,
+        "Prefix length mismatch for known IPv6"
+    );
+    assert!(city_opt_v6.unwrap().country.is_some());
 }
 
 #[test]
 fn test_within_city() {
-    use super::geoip2::City;
-    use super::Within;
-    use ipnetwork::IpNetwork;
-
     let _ = env_logger::try_init();
 
     let filename = "test-data/test-data/GeoIP2-City-Test.mmdb";
 
     let reader = Reader::open_readfile(filename).unwrap();
 
-    let ip_net = IpNetwork::V6("::/0".parse().unwrap());
-
-    let mut iter: Within<City, _> = reader.within(ip_net).unwrap();
+    // --- Test iteration over entire DB ("::/0") ---
+    let ip_net_all = IpNetwork::V6("::/0".parse().unwrap());
+    let mut iter_all: Within<geoip2::City, _> = reader.within(ip_net_all).unwrap();
 
-    // Make sure the first is what we expect it to be
-    let item = iter.next().unwrap().unwrap();
-    assert_eq!(item.ip_net, IpNetwork::V4("2.2.3.0/24".parse().unwrap()));
-    assert_eq!(item.info.city.unwrap().geoname_id, Some(2_655_045));
+    // Get the first item
+    let first_item_result = iter_all.next();
+    assert!(
+        first_item_result.is_some(),
+        "Iterator over ::/0 yielded no items"
+    );
+    let _first_item = first_item_result.unwrap().unwrap();
 
-    let mut n = 1;
-    for _ in iter {
+    // Count the remaining items to check total count
+    let mut n = 1; // Start at 1 since we already took the first item
+    for item_result in iter_all {
+        assert!(item_result.is_ok());
         n += 1;
     }
-
-    // Make sure we had the expected number
     assert_eq!(n, 243);
 
-    // A second run through this time a specific network
+    // --- Test iteration over a specific smaller network ---
     let specific = IpNetwork::V4("81.2.69.0/24".parse().unwrap());
-    let mut iter: Within<City, _> = reader.within(specific).unwrap();
-    // Make sure we have the expected blocks/info
-    let mut expected = vec![
-        // Note: reversed so we can use pop
-        IpNetwork::V4("81.2.69.192/28".parse().unwrap()),
-        IpNetwork::V4("81.2.69.160/27".parse().unwrap()),
-        IpNetwork::V4("81.2.69.144/28".parse().unwrap()),
+    let mut iter_specific: Within<geoip2::City, _> = reader.within(specific).unwrap();
+
+    let expected = vec![
+        // In order of iteration:
         IpNetwork::V4("81.2.69.142/31".parse().unwrap()),
+        IpNetwork::V4("81.2.69.144/28".parse().unwrap()),
+        IpNetwork::V4("81.2.69.160/27".parse().unwrap()),
+        IpNetwork::V4("81.2.69.192/28".parse().unwrap()),
     ];
-    while let Some(e) = expected.pop() {
-        let item = iter.next().unwrap().unwrap();
-        assert_eq!(item.ip_net, e);
+
+    let mut found_count = 0;
+    // Use into_iter() to consume the vector
+    for expected_net in expected.into_iter() {
+        let item_res = iter_specific.next();
+        assert!(
+            item_res.is_some(),
+            "Expected more items in specific iterator"
+        );
+        let item = item_res.unwrap().unwrap();
+        assert_eq!(
+            item.ip_net, expected_net,
+            "Mismatch in specific network iteration"
+        );
+        // Check associated data for one of them
+        if item.ip_net.prefix() == 31 {
+            // 81.2.69.142/31
+            assert!(item.info.city.is_some());
+            assert_eq!(item.info.city.unwrap().geoname_id, Some(2643743)); // London
+        }
+        found_count += 1;
     }
+    assert!(
+        iter_specific.next().is_none(),
+        "Specific iterator should be exhausted after expected items"
+    );
+    assert_eq!(
+        found_count, 4,
+        "Expected exactly 4 networks in 81.2.69.0/24"
+    );
 }
 
-fn check_metadata<T: AsRef<[u8]>>(reader: &Reader<T>, ip_version: usize, record_size: usize) {
+fn check_metadata<S: AsRef<[u8]>>(reader: &Reader<S>, ip_version: usize, record_size: usize) {
     let metadata = &reader.metadata;
 
     assert_eq!(metadata.binary_format_major_version, 2_u16);
-
     assert_eq!(metadata.binary_format_minor_version, 0_u16);
     assert!(metadata.build_epoch >= 1_397_457_605);
     assert_eq!(metadata.database_type, "Test".to_string());
@@ -436,7 +472,7 @@ fn check_metadata<T: AsRef<[u8]>>(reader: &Reader<T>, ip_version: usize, record_
     assert_eq!(metadata.record_size, record_size as u16)
 }
 
-fn check_ip<T: AsRef<[u8]>>(reader: &Reader<T>, ip_version: usize) {
+fn check_ip<S: AsRef<[u8]>>(reader: &Reader<S>, ip_version: usize) {
     let subnets = match ip_version {
         6 => [
             "::1:ffff:ffff",
@@ -459,35 +495,61 @@ fn check_ip<T: AsRef<[u8]>>(reader: &Reader<T>, ip_version: usize) {
         ],
     };
 
-    #[derive(Deserialize, Debug)]
+    #[derive(Deserialize, Debug, PartialEq)]
     struct IpType {
         ip: String,
     }
 
+    // Test lookups that are expected to succeed
     for subnet in &subnets {
         let ip: IpAddr = FromStr::from_str(subnet).unwrap();
-        let value: IpType = reader.lookup(ip).unwrap();
-
+        let result = reader.lookup::<IpType>(ip);
+
+        assert!(
+            result.is_ok(),
+            "Lookup failed unexpectedly for {}: {:?}",
+            subnet,
+            result.err()
+        );
+        let value_option = result.unwrap();
+        assert!(
+            value_option.is_some(),
+            "Lookup for {} returned None unexpectedly",
+            subnet
+        );
+        let value = value_option.unwrap();
+
+        // The value stored is often the network address, not the specific IP looked up
+        // We need to parse the found IP and the subnet IP to check containment or equality.
+        // For the specific MaxMind-DB-test-ipv* files, the stored value IS the looked-up IP string.
         assert_eq!(value.ip, *subnet);
     }
 
+    // Test lookups that are expected to return "not found" (Ok(None))
     let no_record = ["1.1.1.33", "255.254.253.123", "89fa::"];
 
     for &address in &no_record {
-        let ip: IpAddr = FromStr::from_str(address).unwrap();
-        match reader.lookup::<IpType>(ip) {
-            Ok(v) => panic!("received an unexpected value: {v:?}"),
-            Err(e) => assert_eq!(
-                e,
-                MaxMindDBError::AddressNotFoundError("Address not found in database".to_string())
-            ),
+        if ip_version == 4 && address == "89fa::" {
+            continue; // Skip IPv6 address if testing IPv4 db
+        }
+        if ip_version == 6 && address != "89fa::" {
+            continue; // Skip IPv4 addresses if testing IPv6 db
         }
+
+        let ip: IpAddr = FromStr::from_str(address).unwrap();
+        let result = reader.lookup::<IpType>(ip);
+
+        assert!(
+            matches!(result, Ok(None)),
+            "Expected Ok(None) for address {}, but got {:?}",
+            address,
+            result
+        );
     }
 }
 
 #[test]
 fn test_json_serialize() {
-    use super::geoip2::City;
     let _ = env_logger::try_init();
 
     let filename = "test-data/test-data/GeoIP2-City-Test.mmdb";
@@ -495,12 +557,14 @@ fn test_json_serialize() {
     let reader = Reader::open_readfile(filename).unwrap();
 
     let ip: IpAddr = FromStr::from_str("89.160.20.112").unwrap();
-    let city: City = reader.lookup(ip).unwrap();
+    let city: geoip2::City = reader.lookup(ip).unwrap().unwrap();
 
-    let json_string = json!(city).to_string();
+    let json_value = json!(city);
+    let json_string = json_value.to_string();
 
-    assert_eq!(
-        json_string,
-        r#"{"city":{"geoname_id":2694762,"names":{"de":"Linköping","en":"Linköping","fr":"Linköping","ja":"リンシェーピング","zh-CN":"林雪平"}},"continent":{"code":"EU","geoname_id":6255148,"names":{"de":"Europa","en":"Europe","es":"Europa","fr":"Europe","ja":"ヨーロッパ","pt-BR":"Europa","ru":"Европа","zh-CN":"欧洲"}},"country":{"geoname_id":2661886,"is_in_european_union":true,"iso_code":"SE","names":{"de":"Schweden","en":"Sweden","es":"Suecia","fr":"Suède","ja":"スウェーデン王国","pt-BR":"Suécia","ru":"Швеция","zh-CN":"瑞典"}},"location":{"accuracy_radius":76,"latitude":58.4167,"longitude":15.6167,"time_zone":"Europe/Stockholm"},"registered_country":{"geoname_id":2921044,"is_in_european_union":true,"iso_code":"DE","names":{"de":"Deutschland","en":"Germany","es":"Alemania","fr":"Allemagne","ja":"ドイツ連邦共和国","pt-BR":"Alemanha","ru":"Германия","zh-CN":"德国"}},"subdivisions":[{"geoname_id":2685867,"iso_code":"E","names":{"en":"Östergötland County","fr":"Comté d'Östergötland"}}]}"#
-    );
+    let expected_json_str = r#"{"city":{"geoname_id":2694762,"names":{"de":"Linköping","en":"Linköping","fr":"Linköping","ja":"リンシェーピング","zh-CN":"林雪平"}},"continent":{"code":"EU","geoname_id":6255148,"names":{"de":"Europa","en":"Europe","es":"Europa","fr":"Europe","ja":"ヨーロッパ","pt-BR":"Europa","ru":"Европа","zh-CN":"欧洲"}},"country":{"geoname_id":2661886,"is_in_european_union":true,"iso_code":"SE","names":{"de":"Schweden","en":"Sweden","es":"Suecia","fr":"Suède","ja":"スウェーデン王国","pt-BR":"Suécia","ru":"Швеция","zh-CN":"瑞典"}},"location":{"accuracy_radius":76,"latitude":58.4167,"longitude":15.6167,"time_zone":"Europe/Stockholm"},"registered_country":{"geoname_id":2921044,"is_in_european_union":true,"iso_code":"DE","names":{"de":"Deutschland","en":"Germany","es":"Alemania","fr":"Allemagne","ja":"ドイツ連邦共和国","pt-BR":"Alemanha","ru":"Германия","zh-CN":"德国"}},"subdivisions":[{"geoname_id":2685867,"iso_code":"E","names":{"en":"Östergötland County","fr":"Comté d'Östergötland"}}]}"#;
+    let expected_value: serde_json::Value = serde_json::from_str(expected_json_str).unwrap();
+
+    assert_eq!(json_value, expected_value);
+    assert_eq!(json_string, expected_json_str);
 }

From e523f86b14e40c32409e5fc82f2c97f4d51b74a6 Mon Sep 17 00:00:00 2001
From: Gregory Oschwald <goschwald@maxmind.com>
Date: Tue, 25 Mar 2025 11:55:26 -0700
Subject: [PATCH 3/4] Refactor error handling

To allow getting the underlying errors.
---
 CHANGELOG.md                 |  25 +++++-
 Cargo.toml                   |   1 +
 src/maxminddb/decoder.rs     |  33 ++++---
 src/maxminddb/lib.rs         | 164 +++++++++++++++++------------------
 src/maxminddb/reader_test.rs |  17 ++--
 5 files changed, 127 insertions(+), 113 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3d7379b0..e7085304 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,9 +5,32 @@
 * **BREAKING CHANGE:** The `lookup` and `lookup_prefix` methods now return
   `Ok(None)` or `Ok((None, prefix_len))` respectively when an IP address is
   valid but not found in the database (or has no associated data record),
-  instead of returning an `Err(MaxMindDBError::AddressNotFoundError)`.
+  instead of returning an `Err(MaxMindDbError::AddressNotFoundError)`.
   Code previously matching on `AddressNotFoundError` must be updated to
   handle the `Ok(None)` / `Ok((None, prefix_len))` variants.
+* **BREAKING CHANGE:** The `MaxMindDBError` enum  has been renamed
+  `MaxMindDbError` and variants have been renamed and refactored. For
+  example, `IoError` is now `Io`, `InvalidDatabaseError` is now
+  `InvalidDatabase`, `DecodingError` is now `Decoding`,
+  `InvalidNetworkError` is now `InvalidNetwork`. The `MapError` variant has
+  been replaced by `Mmap` (under the `mmap` feature flag). Code explicitly
+  matching on the old variant names must be updated.
+* **BREAKING CHANGE:** `MaxMindDbError` no longer implements `PartialEq`.
+  This is because underlying error types like `std::io::Error` (now
+  wrapped by the `Io` and `Mmap` variants) do not implement `PartialEq`.
+  Code comparing errors directly using `==` or `assert_eq!` must be
+  updated, typically by using `matches!` or by matching on the error
+  kind and potentially its contents.
+* Refactored `MaxMindDbError` handling using the `thiserror` crate.
+  Variants like `Io`, `Mmap`, and `InvalidNetwork` now directly wrap
+  the underlying error types (`std::io::Error`, `ipnetwork::IpNetworkError`).
+* Errors wrapping underlying types (`Io`, `Mmap`, `InvalidNetwork`) now
+  correctly implement `std::error::Error::source()`, allowing inspection
+  of the original cause.
+* The `Display` implementation for `MaxMindDbError` has been refined to
+  generally show only the specific error details, often including the
+  message from the source error, rather than prefixing with the variant
+  name.
 * `lookup_prefix` now returns the prefix length of the entry even when the
   value is not found.
 * Fixed an internal bounds checking error when resolving data pointers.
diff --git a/Cargo.toml b/Cargo.toml
index 738039a9..3c8d9c73 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -30,6 +30,7 @@ serde = { version = "1.0", features = ["derive"] }
 memchr = "2.4"
 memmap2 = { version = "0.9.0", optional = true }
 simdutf8 = { version = "0.1.5", optional = true }
+thiserror = "2.0"
 
 [dev-dependencies]
 env_logger = "0.11"
diff --git a/src/maxminddb/decoder.rs b/src/maxminddb/decoder.rs
index 060aef44..8f1f0508 100644
--- a/src/maxminddb/decoder.rs
+++ b/src/maxminddb/decoder.rs
@@ -4,8 +4,7 @@ use serde::forward_to_deserialize_any;
 use serde::serde_if_integer128;
 use std::convert::TryInto;
 
-use super::MaxMindDBError;
-use super::MaxMindDBError::DecodingError;
+use super::MaxMindDbError;
 
 fn to_usize(base: u8, bytes: &[u8]) -> usize {
     bytes
@@ -134,7 +133,7 @@ impl<'de> Decoder<'de> {
             14 => Value::Bool(self.decode_bool(size)?),
             15 => Value::F32(self.decode_float(size)?),
             u => {
-                return Err(MaxMindDBError::InvalidDatabaseError(format!(
+                return Err(MaxMindDbError::InvalidDatabase(format!(
                     "Unknown data type: {u:?}"
                 )))
             }
@@ -151,7 +150,7 @@ impl<'de> Decoder<'de> {
     fn decode_bool(&mut self, size: usize) -> DecodeResult<bool> {
         match size {
             0 | 1 => Ok(size != 0),
-            s => Err(MaxMindDBError::InvalidDatabaseError(format!(
+            s => Err(MaxMindDbError::InvalidDatabase(format!(
                 "bool of size {s:?}"
             ))),
         }
@@ -170,7 +169,7 @@ impl<'de> Decoder<'de> {
         let value: [u8; 4] = self.buf[self.current_ptr..new_offset]
             .try_into()
             .map_err(|_| {
-                MaxMindDBError::InvalidDatabaseError(format!(
+                MaxMindDbError::InvalidDatabase(format!(
                     "float of size {:?}",
                     new_offset - self.current_ptr
                 ))
@@ -185,7 +184,7 @@ impl<'de> Decoder<'de> {
         let value: [u8; 8] = self.buf[self.current_ptr..new_offset]
             .try_into()
             .map_err(|_| {
-                MaxMindDBError::InvalidDatabaseError(format!(
+                MaxMindDbError::InvalidDatabase(format!(
                     "double of size {:?}",
                     new_offset - self.current_ptr
                 ))
@@ -206,7 +205,7 @@ impl<'de> Decoder<'de> {
                 self.current_ptr = new_offset;
                 Ok(value)
             }
-            s => Err(MaxMindDBError::InvalidDatabaseError(format!(
+            s => Err(MaxMindDbError::InvalidDatabase(format!(
                 "u64 of size {s:?}"
             ))),
         }
@@ -227,7 +226,7 @@ impl<'de> Decoder<'de> {
                     self.current_ptr = new_offset;
                     Ok(value)
                 }
-                s => Err(MaxMindDBError::InvalidDatabaseError(format!(
+                s => Err(MaxMindDbError::InvalidDatabase(format!(
                     "u128 of size {s:?}"
                 ))),
             }
@@ -245,7 +244,7 @@ impl<'de> Decoder<'de> {
                 self.current_ptr = new_offset;
                 Ok(value)
             }
-            s => Err(MaxMindDBError::InvalidDatabaseError(format!(
+            s => Err(MaxMindDbError::InvalidDatabase(format!(
                 "u32 of size {s:?}"
             ))),
         }
@@ -262,7 +261,7 @@ impl<'de> Decoder<'de> {
                 self.current_ptr = new_offset;
                 Ok(value)
             }
-            s => Err(MaxMindDBError::InvalidDatabaseError(format!(
+            s => Err(MaxMindDbError::InvalidDatabase(format!(
                 "u16 of size {s:?}"
             ))),
         }
@@ -279,7 +278,7 @@ impl<'de> Decoder<'de> {
                 self.current_ptr = new_offset;
                 Ok(value)
             }
-            s => Err(MaxMindDBError::InvalidDatabaseError(format!(
+            s => Err(MaxMindDbError::InvalidDatabase(format!(
                 "int32 of size {s:?}"
             ))),
         }
@@ -338,17 +337,17 @@ impl<'de> Decoder<'de> {
         self.current_ptr = new_offset;
         match from_utf8(bytes) {
             Ok(v) => Ok(v),
-            Err(_) => Err(MaxMindDBError::InvalidDatabaseError(
+            Err(_) => Err(MaxMindDbError::InvalidDatabase(
                 "error decoding string".to_owned(),
             )),
         }
     }
 }
 
-pub type DecodeResult<T> = Result<T, MaxMindDBError>;
+pub type DecodeResult<T> = Result<T, MaxMindDbError>;
 
 impl<'de: 'a, 'a> de::Deserializer<'de> for &'a mut Decoder<'de> {
-    type Error = MaxMindDBError;
+    type Error = MaxMindDbError;
 
     fn deserialize_any<V>(self, visitor: V) -> DecodeResult<V::Value>
     where
@@ -383,7 +382,7 @@ struct ArrayAccess<'a, 'de: 'a> {
 // `SeqAccess` is provided to the `Visitor` to give it the ability to iterate
 // through elements of the sequence.
 impl<'de> SeqAccess<'de> for ArrayAccess<'_, 'de> {
-    type Error = MaxMindDBError;
+    type Error = MaxMindDbError;
 
     fn next_element_seed<T>(&mut self, seed: T) -> DecodeResult<Option<T::Value>>
     where
@@ -408,7 +407,7 @@ struct MapAccessor<'a, 'de: 'a> {
 // `MapAccess` is provided to the `Visitor` to give it the ability to iterate
 // through entries of the map.
 impl<'de> MapAccess<'de> for MapAccessor<'_, 'de> {
-    type Error = MaxMindDBError;
+    type Error = MaxMindDbError;
 
     fn next_key_seed<K>(&mut self, seed: K) -> DecodeResult<Option<K::Value>>
     where
@@ -430,7 +429,7 @@ impl<'de> MapAccess<'de> for MapAccessor<'_, 'de> {
     {
         // Check if there are no more entries.
         if self.count == 0 {
-            return Err(DecodingError("no more entries".to_owned()));
+            return Err(MaxMindDbError::Decoding("no more entries".to_owned()));
         }
         self.count -= 1;
 
diff --git a/src/maxminddb/lib.rs b/src/maxminddb/lib.rs
index b8c84a54..876e8989 100644
--- a/src/maxminddb/lib.rs
+++ b/src/maxminddb/lib.rs
@@ -2,14 +2,16 @@
 
 use std::cmp::Ordering;
 use std::collections::BTreeMap;
-use std::fmt::{self, Display, Formatter};
+use std::fmt::Display;
+use std::fs;
 use std::io;
 use std::marker::PhantomData;
 use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
 use std::path::Path;
 
-use ipnetwork::IpNetwork;
+use ipnetwork::{IpNetwork, IpNetworkError};
 use serde::{de, Deserialize, Serialize};
+use thiserror::Error;
 
 #[cfg(feature = "mmap")]
 pub use memmap2::Mmap;
@@ -21,43 +23,36 @@ use std::fs::File;
 #[cfg(all(feature = "simdutf8", feature = "unsafe-str-decode"))]
 compile_error!("features `simdutf8` and `unsafe-str-decode` are mutually exclusive");
 
-#[derive(Debug, PartialEq, Eq)]
-pub enum MaxMindDBError {
-    InvalidDatabaseError(String),
-    IoError(String),
-    MapError(String),
-    DecodingError(String),
-    InvalidNetworkError(String),
+#[derive(Error, Debug)]
+pub enum MaxMindDbError {
+    #[error("Invalid database: {0}")]
+    InvalidDatabase(String),
+
+    #[error("I/O error: {0}")]
+    Io(
+        #[from]
+        #[source]
+        io::Error,
+    ),
+
+    #[cfg(feature = "mmap")]
+    #[error("Memory map error: {0}")]
+    Mmap(#[source] io::Error),
+
+    #[error("Decoding error: {0}")]
+    Decoding(String),
+
+    #[error("Invalid network: {0}")]
+    InvalidNetwork(
+        #[from]
+        #[source]
+        IpNetworkError,
+    ),
 }
 
-impl From<io::Error> for MaxMindDBError {
-    fn from(err: io::Error) -> MaxMindDBError {
-        // clean up and clean up MaxMindDBError generally
-        MaxMindDBError::IoError(err.to_string())
-    }
-}
-
-impl Display for MaxMindDBError {
-    fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), fmt::Error> {
-        match self {
-            MaxMindDBError::InvalidDatabaseError(msg) => {
-                write!(fmt, "InvalidDatabaseError: {msg}")?
-            }
-            MaxMindDBError::IoError(msg) => write!(fmt, "IoError: {msg}")?,
-            MaxMindDBError::MapError(msg) => write!(fmt, "MapError: {msg}")?,
-            MaxMindDBError::DecodingError(msg) => write!(fmt, "DecodingError: {msg}")?,
-            MaxMindDBError::InvalidNetworkError(msg) => write!(fmt, "InvalidNetworkError: {msg}")?,
-        }
-        Ok(())
-    }
-}
-
-// Use default implementation for `std::error::Error`
-impl std::error::Error for MaxMindDBError {}
-
-impl de::Error for MaxMindDBError {
+impl de::Error for MaxMindDbError {
     fn custom<T: Display>(msg: T) -> Self {
-        MaxMindDBError::DecodingError(format!("{msg}"))
+        MaxMindDbError::Decoding(format!("{msg}"))
     }
 }
 
@@ -132,7 +127,7 @@ impl IpInt {
 }
 
 impl<'de, T: Deserialize<'de>, S: AsRef<[u8]>> Iterator for Within<'de, T, S> {
-    type Item = Result<WithinItem<T>, MaxMindDBError>;
+    type Item = Result<WithinItem<T>, MaxMindDbError>;
 
     fn next(&mut self) -> Option<Self::Item> {
         while let Some(current) = self.stack.pop() {
@@ -232,9 +227,9 @@ impl<'de> Reader<Mmap> {
     /// let reader = maxminddb::Reader::open_mmap("test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
     /// # }
     /// ```
-    pub fn open_mmap<P: AsRef<Path>>(database: P) -> Result<Reader<Mmap>, MaxMindDBError> {
+    pub fn open_mmap<P: AsRef<Path>>(database: P) -> Result<Reader<Mmap>, MaxMindDbError> {
         let file_read = File::open(database)?;
-        let mmap = unsafe { MmapOptions::new().map(&file_read) }?;
+        let mmap = unsafe { MmapOptions::new().map(&file_read) }.map_err(MaxMindDbError::Mmap)?;
         Reader::from_source(mmap)
     }
 }
@@ -247,10 +242,8 @@ impl Reader<Vec<u8>> {
     /// ```
     /// let reader = maxminddb::Reader::open_readfile("test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
     /// ```
-    pub fn open_readfile<P: AsRef<Path>>(database: P) -> Result<Reader<Vec<u8>>, MaxMindDBError> {
-        use std::fs;
-
-        let buf: Vec<u8> = fs::read(&database)?;
+    pub fn open_readfile<P: AsRef<Path>>(database: P) -> Result<Reader<Vec<u8>>, MaxMindDbError> {
+        let buf: Vec<u8> = fs::read(&database)?; // IO error converted via #[from]
         Reader::from_source(buf)
     }
 }
@@ -265,7 +258,7 @@ impl<'de, S: AsRef<[u8]>> Reader<S> {
     /// let buf = fs::read("test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
     /// let reader = maxminddb::Reader::from_source(buf).unwrap();
     /// ```
-    pub fn from_source(buf: S) -> Result<Reader<S>, MaxMindDBError> {
+    pub fn from_source(buf: S) -> Result<Reader<S>, MaxMindDbError> {
         let data_section_separator_size = 16;
 
         let metadata_start = find_metadata_start(buf.as_ref())?;
@@ -294,7 +287,7 @@ impl<'de, S: AsRef<[u8]>> Reader<S> {
     /// # use maxminddb::geoip2;
     /// # use std::net::IpAddr;
     /// # use std::str::FromStr;
-    /// # fn main() -> Result<(), maxminddb::MaxMindDBError> {
+    /// # fn main() -> Result<(), maxminddb::MaxMindDbError> {
     /// let reader = maxminddb::Reader::open_readfile("test-data/test-data/GeoIP2-City-Test.mmdb")?;
     ///
     /// let ip: IpAddr = FromStr::from_str("89.160.20.128").unwrap();
@@ -306,7 +299,7 @@ impl<'de, S: AsRef<[u8]>> Reader<S> {
     /// # Ok(())
     /// # }
     /// ```
-    pub fn lookup<T>(&'de self, address: IpAddr) -> Result<Option<T>, MaxMindDBError>
+    pub fn lookup<T>(&'de self, address: IpAddr) -> Result<Option<T>, MaxMindDbError>
     where
         T: Deserialize<'de>,
     {
@@ -326,7 +319,7 @@ impl<'de, S: AsRef<[u8]>> Reader<S> {
     /// # use maxminddb::geoip2;
     /// # use std::net::IpAddr;
     /// # use std::str::FromStr;
-    /// # fn main() -> Result<(), maxminddb::MaxMindDBError> {
+    /// # fn main() -> Result<(), maxminddb::MaxMindDbError> {
     /// let reader = maxminddb::Reader::open_readfile("test-data/test-data/GeoIP2-City-Test.mmdb")?;
     ///
     /// let ip: IpAddr = "89.160.20.128".parse().unwrap(); // Known IP
@@ -349,12 +342,12 @@ impl<'de, S: AsRef<[u8]>> Reader<S> {
     pub fn lookup_prefix<T>(
         &'de self,
         address: IpAddr,
-    ) -> Result<(Option<T>, usize), MaxMindDBError>
+    ) -> Result<(Option<T>, usize), MaxMindDbError>
     where
         T: Deserialize<'de>,
     {
         let ip_int = IpInt::new(address);
-        // find_address_in_tree returns Result<(usize, usize), MaxMindDBError> -> (pointer, prefix_len)
+        // find_address_in_tree returns Result<(usize, usize), MaxMindDbError> -> (pointer, prefix_len)
         let (pointer, prefix_len) = self.find_address_in_tree(&ip_int)?;
 
         if pointer == 0 {
@@ -391,7 +384,7 @@ impl<'de, S: AsRef<[u8]>> Reader<S> {
     ///     println!("ip_net={}, city={:?}", item.ip_net, item.info);
     /// }
     /// ```
-    pub fn within<T>(&'de self, cidr: IpNetwork) -> Result<Within<'de, T, S>, MaxMindDBError>
+    pub fn within<T>(&'de self, cidr: IpNetwork) -> Result<Within<'de, T, S>, MaxMindDbError>
     where
         T: Deserialize<'de>,
     {
@@ -440,7 +433,7 @@ impl<'de, S: AsRef<[u8]>> Reader<S> {
         Ok(within)
     }
 
-    fn find_address_in_tree(&self, ip_int: &IpInt) -> Result<(usize, usize), MaxMindDBError> {
+    fn find_address_in_tree(&self, ip_int: &IpInt) -> Result<(usize, usize), MaxMindDbError> {
         let bit_count = ip_int.bit_count();
         let mut node = self.start_node(bit_count);
 
@@ -460,7 +453,7 @@ impl<'de, S: AsRef<[u8]>> Reader<S> {
             // return 0 as the pointer value to signify "not found".
             n if n == node => Ok((0, prefix_len)),
             n if node > n => Ok((node, prefix_len)),
-            _ => Err(MaxMindDBError::InvalidDatabaseError(
+            _ => Err(MaxMindDbError::InvalidDatabase(
                 "invalid node in search tree".to_owned(),
             )),
         }
@@ -474,7 +467,7 @@ impl<'de, S: AsRef<[u8]>> Reader<S> {
         }
     }
 
-    fn find_ipv4_start(&self) -> Result<usize, MaxMindDBError> {
+    fn find_ipv4_start(&self) -> Result<usize, MaxMindDbError> {
         if self.metadata.ip_version != 6 {
             return Ok(0);
         }
@@ -491,7 +484,7 @@ impl<'de, S: AsRef<[u8]>> Reader<S> {
         Ok(node)
     }
 
-    fn read_node(&self, node_number: usize, index: usize) -> Result<usize, MaxMindDBError> {
+    fn read_node(&self, node_number: usize, index: usize) -> Result<usize, MaxMindDbError> {
         let buf = self.buf.as_ref();
         let base_offset = node_number * (self.metadata.record_size as usize) / 4;
 
@@ -515,7 +508,7 @@ impl<'de, S: AsRef<[u8]>> Reader<S> {
                 to_usize(0, &buf[offset..offset + 4])
             }
             s => {
-                return Err(MaxMindDBError::InvalidDatabaseError(format!(
+                return Err(MaxMindDbError::InvalidDatabase(format!(
                     "unknown record size: \
                      {s:?}"
                 )))
@@ -524,12 +517,12 @@ impl<'de, S: AsRef<[u8]>> Reader<S> {
         Ok(val)
     }
 
-    fn resolve_data_pointer(&self, pointer: usize) -> Result<usize, MaxMindDBError> {
+    fn resolve_data_pointer(&self, pointer: usize) -> Result<usize, MaxMindDbError> {
         let resolved = pointer - (self.metadata.node_count as usize) - 16;
 
         // Check bounds using pointer_base which marks the start of the data section
         if resolved >= (self.buf.as_ref().len() - self.pointer_base) {
-            return Err(MaxMindDBError::InvalidDatabaseError(
+            return Err(MaxMindDbError::InvalidDatabase(
                 "the MaxMind DB file's data pointer resolves to an invalid location".to_owned(),
             ));
         }
@@ -547,7 +540,7 @@ fn to_usize(base: u8, bytes: &[u8]) -> usize {
 }
 
 #[inline]
-fn bytes_and_prefix_to_net(bytes: &IpInt, prefix: u8) -> Result<IpNetwork, MaxMindDBError> {
+fn bytes_and_prefix_to_net(bytes: &IpInt, prefix: u8) -> Result<IpNetwork, MaxMindDbError> {
     let (ip, prefix) = match bytes {
         IpInt::V4(ip) => (IpAddr::V4(Ipv4Addr::from(*ip)), prefix),
         IpInt::V6(ip) if bytes.is_ipv4_in_ipv6() => {
@@ -555,16 +548,16 @@ fn bytes_and_prefix_to_net(bytes: &IpInt, prefix: u8) -> Result<IpNetwork, MaxMi
         }
         IpInt::V6(ip) => (IpAddr::V6(Ipv6Addr::from(*ip)), prefix),
     };
-    IpNetwork::new(ip, prefix).map_err(|e| MaxMindDBError::InvalidNetworkError(e.to_string()))
+    IpNetwork::new(ip, prefix).map_err(MaxMindDbError::InvalidNetwork)
 }
 
-fn find_metadata_start(buf: &[u8]) -> Result<usize, MaxMindDBError> {
+fn find_metadata_start(buf: &[u8]) -> Result<usize, MaxMindDbError> {
     const METADATA_START_MARKER: &[u8] = b"\xab\xcd\xefMaxMind.com";
 
     memchr::memmem::rfind(buf, METADATA_START_MARKER)
         .map(|x| x + METADATA_START_MARKER.len())
         .ok_or_else(|| {
-            MaxMindDBError::InvalidDatabaseError(
+            MaxMindDbError::InvalidDatabase(
                 "Could not find MaxMind DB metadata in file.".to_owned(),
             )
         })
@@ -578,44 +571,43 @@ mod reader_test;
 
 #[cfg(test)]
 mod tests {
-    use super::MaxMindDBError;
+    use super::MaxMindDbError;
+    use ipnetwork::IpNetworkError;
+    use std::io::{Error, ErrorKind};
 
     #[test]
     fn test_error_display() {
         assert_eq!(
             format!(
                 "{}",
-                MaxMindDBError::InvalidDatabaseError("something went wrong".to_owned())
+                MaxMindDbError::InvalidDatabase("something went wrong".to_owned())
             ),
-            "InvalidDatabaseError: something went wrong".to_owned(),
+            "Invalid database: something went wrong".to_owned(),
         );
+        let io_err = Error::new(ErrorKind::NotFound, "file not found");
         assert_eq!(
-            format!(
-                "{}",
-                MaxMindDBError::IoError("something went wrong".to_owned())
-            ),
-            "IoError: something went wrong".to_owned(),
-        );
-        assert_eq!(
-            format!(
-                "{}",
-                MaxMindDBError::MapError("something went wrong".to_owned())
-            ),
-            "MapError: something went wrong".to_owned(),
+            format!("{}", MaxMindDbError::from(io_err)),
+            "I/O error: file not found".to_owned(),
         );
+
+        #[cfg(feature = "mmap")]
+        {
+            let mmap_io_err = Error::new(ErrorKind::PermissionDenied, "mmap failed");
+            assert_eq!(
+                format!("{}", MaxMindDbError::Mmap(mmap_io_err)),
+                "Memory map error: mmap failed".to_owned(),
+            );
+        }
+
         assert_eq!(
-            format!(
-                "{}",
-                MaxMindDBError::DecodingError("something went wrong".to_owned())
-            ),
-            "DecodingError: something went wrong".to_owned(),
+            format!("{}", MaxMindDbError::Decoding("unexpected type".to_owned())),
+            "Decoding error: unexpected type".to_owned(),
         );
+
+        let net_err = IpNetworkError::InvalidPrefix;
         assert_eq!(
-            format!(
-                "{}",
-                MaxMindDBError::InvalidNetworkError("something went wrong".to_owned())
-            ),
-            "InvalidNetworkError: something went wrong".to_owned(),
+            format!("{}", MaxMindDbError::from(net_err)),
+            "Invalid network: invalid prefix".to_owned(),
         );
     }
 
diff --git a/src/maxminddb/reader_test.rs b/src/maxminddb/reader_test.rs
index dbaa9ad6..7c553b00 100644
--- a/src/maxminddb/reader_test.rs
+++ b/src/maxminddb/reader_test.rs
@@ -6,7 +6,7 @@ use serde::Deserialize;
 use serde_json::json;
 
 use crate::geoip2;
-use crate::{MaxMindDBError, Reader, Within};
+use crate::{MaxMindDbError, Reader, Within};
 
 #[allow(clippy::float_cmp)]
 #[test]
@@ -106,10 +106,10 @@ fn test_broken_database() {
     match r.lookup::<TestType>(ip) {
         Err(e) => assert!(matches!(
             e,
-            MaxMindDBError::InvalidDatabaseError(_) // Check variant, message might vary slightly
+            MaxMindDbError::InvalidDatabase(_) // Check variant, message might vary slightly
         )),
         Ok(Some(_)) => panic!("Unexpected success with broken data"),
-        Ok(None) => panic!("Got None, expected InvalidDatabaseError"),
+        Ok(None) => panic!("Got None, expected InvalidDatabase"),
     }
 }
 
@@ -120,7 +120,7 @@ fn test_missing_database() {
     let r = Reader::open_readfile("file-does-not-exist.mmdb");
     match r {
         Ok(_) => panic!("Received Reader when opening non-existent file"),
-        Err(e) => assert!(matches!(e, MaxMindDBError::IoError(_))), // Specific message might vary by OS/locale
+        Err(e) => assert!(matches!(e, MaxMindDbError::Io(_))), // Specific message might vary by OS/locale
     }
 }
 
@@ -131,11 +131,10 @@ fn test_non_database() {
     let r = Reader::open_readfile("README.md");
     match r {
         Ok(_) => panic!("Received Reader when opening a non-MMDB file"),
-        Err(e) => assert_eq!(
-            e,
-            MaxMindDBError::InvalidDatabaseError(
-                "Could not find MaxMind DB metadata in file.".to_owned(),
-            )
+        Err(e) => assert!(
+            matches!(&e, MaxMindDbError::InvalidDatabase(s) if s == "Could not find MaxMind DB metadata in file."),
+            "Expected InvalidDatabase error with specific message, but got: {:?}",
+            e
         ),
     }
 }

From 1615c748de6312adf65c087205c7b258dbdc6ed8 Mon Sep 17 00:00:00 2001
From: Gregory Oschwald <goschwald@maxmind.com>
Date: Tue, 25 Mar 2025 12:56:01 -0700
Subject: [PATCH 4/4] Refactor repeated code into function

---
 src/maxminddb/lib.rs | 37 ++++++++++++++++++++-----------------
 1 file changed, 20 insertions(+), 17 deletions(-)

diff --git a/src/maxminddb/lib.rs b/src/maxminddb/lib.rs
index 876e8989..dfd0b48f 100644
--- a/src/maxminddb/lib.rs
+++ b/src/maxminddb/lib.rs
@@ -150,16 +150,9 @@ impl<'de, T: Deserialize<'de>, S: AsRef<[u8]>> Iterator for Within<'de, T, S> {
                             Ok(ip_net) => ip_net,
                             Err(e) => return Some(Err(e)),
                         };
-                    // TODO: should this block become a helper method on reader?
-                    let rec = match self.reader.resolve_data_pointer(current.node) {
-                        Ok(rec) => rec,
-                        Err(e) => return Some(Err(e)),
-                    };
-                    let mut decoder = decoder::Decoder::new(
-                        &self.reader.buf.as_ref()[self.reader.pointer_base..],
-                        rec,
-                    );
-                    return match T::deserialize(&mut decoder) {
+
+                    // Call the new helper method to decode data
+                    return match self.reader.decode_data_at_pointer(current.node) {
                         Ok(info) => Some(Ok(WithinItem { ip_net, info })),
                         Err(e) => Some(Err(e)),
                     };
@@ -356,12 +349,8 @@ impl<'de, S: AsRef<[u8]>> Reader<S> {
             return Ok((None, prefix_len));
         }
 
-        // If pointer > 0, attempt to resolve and decode data
-        let rec = self.resolve_data_pointer(pointer)?;
-        let mut decoder = decoder::Decoder::new(&self.buf.as_ref()[self.pointer_base..], rec);
-
-        // Attempt to deserialize. If successful, wrap in Some. If error, propagate.
-        match T::deserialize(&mut decoder) {
+        // If pointer > 0, attempt to resolve and decode data using the helper method
+        match self.decode_data_at_pointer(pointer) {
             Ok(value) => Ok((Some(value), prefix_len)),
             Err(e) => Err(e),
         }
@@ -517,6 +506,7 @@ impl<'de, S: AsRef<[u8]>> Reader<S> {
         Ok(val)
     }
 
+    /// Resolves a pointer from the search tree to an offset in the data section.
     fn resolve_data_pointer(&self, pointer: usize) -> Result<usize, MaxMindDbError> {
         let resolved = pointer - (self.metadata.node_count as usize) - 16;
 
@@ -529,6 +519,18 @@ impl<'de, S: AsRef<[u8]>> Reader<S> {
 
         Ok(resolved)
     }
+
+    /// Decodes data at the given pointer offset.
+    /// Assumes the pointer is valid and points to the data section.
+    fn decode_data_at_pointer<T>(&'de self, pointer: usize) -> Result<T, MaxMindDbError>
+    where
+        T: Deserialize<'de>,
+    {
+        let resolved_offset = self.resolve_data_pointer(pointer)?;
+        let mut decoder =
+            decoder::Decoder::new(&self.buf.as_ref()[self.pointer_base..], resolved_offset);
+        T::deserialize(&mut decoder)
+    }
 }
 
 // I haven't moved all patterns of this form to a generic function as
@@ -630,7 +632,8 @@ mod tests {
         let result_lookup_prefix = reader.lookup_prefix::<geoip2::City>(ip);
         assert!(
             matches!(result_lookup_prefix, Ok((None, 8))),
-            "lookup_prefix should return Ok(None) for unknown IP"
+            "lookup_prefix should return Ok((None, 8)) for unknown IP, got {:?}",
+            result_lookup_prefix
         );
     }