maxminddb 0.31.0

Library for reading MaxMind DB format used by GeoIP2 and GeoLite2
Documentation
//! Error types for MaxMind DB operations.

use std::fmt::Display;
use std::io;

use ipnetwork::IpNetworkError;
use serde::de;
use thiserror::Error;

/// Error returned by MaxMind DB operations.
#[derive(Error, Debug)]
#[non_exhaustive]
pub enum MaxMindDbError {
    /// The database file is invalid or corrupted.
    #[error("{}", format_invalid_database(.message, .offset))]
    InvalidDatabase {
        /// Description of what is invalid.
        message: String,
        /// Byte offset where the error was detected. Reader operations report
        /// an absolute database-file offset; decoding a standalone section
        /// reports an offset relative to that input.
        offset: Option<usize>,
    },

    /// An I/O error occurred while reading the database.
    #[error("i/o error: {0}")]
    Io(
        #[from]
        #[source]
        io::Error,
    ),

    /// Memory mapping failed.
    #[cfg(feature = "mmap")]
    #[error("memory map error: {0}")]
    Mmap(#[source] io::Error),

    /// Error decoding data from the database.
    #[error(
        "{}",
        format_contextual_error("decoding error", .message, .offset, .path.as_deref())
    )]
    Decoding {
        /// Description of the decoding error.
        message: String,
        /// Byte offset relative to the section being decoded: the data section
        /// for records, or the metadata value after its marker for metadata.
        offset: Option<usize>,
        /// JSON-pointer-like path to the field (e.g., "/city/names/en").
        path: Option<String>,
    },

    /// Decoding or verification stopped because it exceeded an expansion or
    /// work safety limit.
    ///
    /// This does not necessarily mean that the database is structurally
    /// invalid. Schema-specific limits reported by a custom Serde visitor use
    /// [`MaxMindDbError::Decoding`] instead. Applications may choose a narrower
    /// schema or reject the database as untrusted input.
    #[error(
        "{}",
        format_contextual_error("resource limit exceeded", .message, .offset, .path.as_deref())
    )]
    ResourceLimit {
        /// Description of the limit that was exceeded.
        message: String,
        /// Byte offset relative to the section being decoded: the data section
        /// for records, or the metadata value after its marker for metadata.
        offset: Option<usize>,
        /// JSON-pointer-like path to the field (e.g., "/subdivisions").
        path: Option<String>,
    },

    /// The provided network/CIDR is invalid.
    #[error("invalid network: {0}")]
    InvalidNetwork(
        #[from]
        #[source]
        IpNetworkError,
    ),

    /// The provided input is invalid for this operation.
    #[error("invalid input: {message}")]
    InvalidInput {
        /// Description of what is invalid about the input.
        message: String,
    },
}

fn format_invalid_database(message: &str, offset: &Option<usize>) -> String {
    match offset {
        Some(off) => format!("invalid database at offset {off}: {message}"),
        None => format!("invalid database: {message}"),
    }
}

fn format_contextual_error(
    prefix: &str,
    message: &str,
    offset: &Option<usize>,
    path: Option<&str>,
) -> String {
    match (offset, path) {
        (Some(off), Some(p)) => format!("{prefix} at offset {off} (path: {p}): {message}"),
        (Some(off), None) => format!("{prefix} at offset {off}: {message}"),
        (None, Some(p)) => format!("{prefix} (path: {p}): {message}"),
        (None, None) => format!("{prefix}: {message}"),
    }
}

impl MaxMindDbError {
    /// Creates an InvalidDatabase error with just a message.
    pub fn invalid_database(message: impl Into<String>) -> Self {
        MaxMindDbError::InvalidDatabase {
            message: message.into(),
            offset: None,
        }
    }

    /// Creates an InvalidDatabase error with message and offset.
    pub fn invalid_database_at(message: impl Into<String>, offset: usize) -> Self {
        MaxMindDbError::InvalidDatabase {
            message: message.into(),
            offset: Some(offset),
        }
    }

    /// Creates a Decoding error with just a message.
    pub fn decoding(message: impl Into<String>) -> Self {
        MaxMindDbError::Decoding {
            message: message.into(),
            offset: None,
            path: None,
        }
    }

    /// Creates a Decoding error with message and offset.
    pub fn decoding_at(message: impl Into<String>, offset: usize) -> Self {
        MaxMindDbError::Decoding {
            message: message.into(),
            offset: Some(offset),
            path: None,
        }
    }

    /// Creates a Decoding error with message, offset, and path.
    pub fn decoding_at_path(
        message: impl Into<String>,
        offset: usize,
        path: impl Into<String>,
    ) -> Self {
        MaxMindDbError::Decoding {
            message: message.into(),
            offset: Some(offset),
            path: Some(path.into()),
        }
    }

    /// Creates a ResourceLimit error with a message and offset.
    pub fn resource_limit_at(message: impl Into<String>, offset: usize) -> Self {
        MaxMindDbError::ResourceLimit {
            message: message.into(),
            offset: Some(offset),
            path: None,
        }
    }

    /// Translate a decoder-originated invalid-database offset from a section
    /// into the containing database. Other error variants intentionally retain
    /// their documented section-relative offsets.
    pub(crate) fn with_invalid_database_offset_base(self, base: usize) -> Self {
        match self {
            MaxMindDbError::InvalidDatabase {
                message,
                offset: Some(offset),
            } => MaxMindDbError::InvalidDatabase {
                message,
                offset: offset.checked_add(base),
            },
            _ => self,
        }
    }

    /// Creates an InvalidInput error.
    pub fn invalid_input(message: impl Into<String>) -> Self {
        MaxMindDbError::InvalidInput {
            message: message.into(),
        }
    }
}

impl de::Error for MaxMindDbError {
    fn custom<T: Display>(msg: T) -> Self {
        MaxMindDbError::decoding(msg.to_string())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::io::{Error, ErrorKind};

    #[test]
    fn test_error_display() {
        // Error without offset
        assert_eq!(
            format!(
                "{}",
                MaxMindDbError::invalid_database("something went wrong")
            ),
            "invalid database: something went wrong".to_owned(),
        );
        // Error with offset
        assert_eq!(
            format!(
                "{}",
                MaxMindDbError::invalid_database_at("something went wrong", 42)
            ),
            "invalid database at offset 42: something went wrong".to_owned(),
        );
        let io_err = Error::new(ErrorKind::NotFound, "file not found");
        assert_eq!(
            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(),
            );
        }

        // Decoding error without offset
        assert_eq!(
            format!("{}", MaxMindDbError::decoding("unexpected type")),
            "decoding error: unexpected type".to_owned(),
        );
        // Decoding error with offset
        assert_eq!(
            format!("{}", MaxMindDbError::decoding_at("unexpected type", 100)),
            "decoding error at offset 100: unexpected type".to_owned(),
        );
        // Decoding error with offset and path
        assert_eq!(
            format!(
                "{}",
                MaxMindDbError::decoding_at_path("unexpected type", 100, "/city/names/en")
            ),
            "decoding error at offset 100 (path: /city/names/en): unexpected type".to_owned(),
        );

        assert_eq!(
            format!(
                "{}",
                MaxMindDbError::resource_limit_at("too many values", 100)
            ),
            "resource limit exceeded at offset 100: too many values".to_owned(),
        );

        let net_err = IpNetworkError::InvalidPrefix;
        assert_eq!(
            format!("{}", MaxMindDbError::from(net_err)),
            "invalid network: invalid prefix".to_owned(),
        );

        // InvalidInput error
        assert_eq!(
            format!("{}", MaxMindDbError::invalid_input("bad address")),
            "invalid input: bad address".to_owned(),
        );
    }
}