xdb-parse 0.2.0

High-performance, zero-copy xdb IP geolocation parser (ip2region-compatible)
Documentation
//! Xdb file metadata: header parsing and IP version detection.

use crate::error::XdbError;
use crate::{IPV4_SEGMENT_INDEX_BLOCK_SIZE, IPV6_SEGMENT_INDEX_BLOCK_SIZE};

/// IP protocol version of an xdb database.
///
/// Returned by [`detect_version`] and [`Header::ip_version`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IpVersion {
    /// IPv4 xdb — segments are 14 bytes each.
    V4,
    /// IPv6 xdb — segments are 38 bytes each.
    V6,
}

/// Parsed xdb file header (first 16 bytes of meaningful data in the 256-byte header block).
///
/// # Fields
///
/// - `version` — xdb format version (typically 2)
/// - `cache_type` — cache strategy hint
/// - `generation_time` — Unix timestamp of database generation
/// - `index_base_address` — file offset where segment index starts
/// - `index_end_address` — file offset where segment index ends (inclusive)
///
/// # Example
///
/// ```no_run
/// let data = xdb_parse::load_file("./assets/ip2region_v4.xdb".into())?;
/// let header = xdb_parse::Header::parse(&data)?;
/// println!("xdb v{}, generated at {}", header.version, header.generation_time);
/// # Ok::<(), xdb_parse::error::XdbError>(())
/// ```
#[derive(Debug, Clone, Default)]
pub struct Header {
    pub version: u16,
    pub cache_type: u16,
    pub generation_time: u32,
    pub index_base_address: u32,
    pub index_end_address: u32,
}

impl Header {
    /// Parse the header from the first bytes of an xdb file.
    ///
    /// Reads fields from the header block (offsets 0..16 within the 256-byte header).
    pub fn parse(data: &[u8]) -> Result<Self, XdbError> {
        Ok(Header {
            version: u16::from_ne_bytes(data[0..2].try_into()?),
            cache_type: u16::from_ne_bytes(data[2..4].try_into()?),
            generation_time: u32::from_ne_bytes(data[4..8].try_into()?),
            index_base_address: u32::from_ne_bytes(data[8..12].try_into()?),
            index_end_address: u32::from_ne_bytes(data[12..16].try_into()?),
        })
    }

    /// Detect the IP version by examining the segment index size.
    ///
    /// Uses the segment index range (`index_end_address - index_base_address`)
    /// to determine whether segments are 14 bytes (IPv4) or 38 bytes (IPv6).
    pub fn ip_version(&self) -> Result<IpVersion, XdbError> {
        let len = self.index_end_address - self.index_base_address + 1;
        if len % IPV4_SEGMENT_INDEX_BLOCK_SIZE <= 1 {
            Ok(IpVersion::V4)
        } else if len % IPV6_SEGMENT_INDEX_BLOCK_SIZE <= 1 {
            Ok(IpVersion::V6)
        } else {
            Err(XdbError::InvalidIPVersion("unknown xdb version".into()))
        }
    }
}

/// Detect the IP version from loaded xdb data.
///
/// Parses the header and calls [`Header::ip_version`]. This is a convenience
/// function — use [`Header::parse`] directly if you also need other header fields.
///
/// # Example
///
/// ```no_run
/// let data = xdb_parse::load_file("./assets/ip2region_v4.xdb".into())?;
/// let version = xdb_parse::detect_version(&data)?;
/// assert_eq!(version, xdb_parse::IpVersion::V4);
/// # Ok::<(), xdb_parse::error::XdbError>(())
/// ```
pub fn detect_version(data: &[u8]) -> Result<IpVersion, XdbError> {
    Header::parse(data)?.ip_version()
}

#[cfg(test)]
mod tests {
    use super::*;
    use anyhow::Result;

    #[test]
    fn test_parse_header() -> Result<()> {
        let mut buf = vec![0u8; 256];
        buf[0..2].copy_from_slice(&2u16.to_ne_bytes());
        let header = Header::parse(&buf)?;
        assert_eq!(header.version, 2);
        Ok(())
    }

    #[test]
    fn test_detect_version_v4() -> Result<()> {
        let path = "./assets/ip2region_v4.xdb";
        let data = std::fs::read(path)?;
        let version = detect_version(&data)?;
        assert_eq!(version, IpVersion::V4);
        Ok(())
    }

    #[test]
    fn test_detect_version_v6() -> Result<()> {
        let path = "./assets/ip2region_v6.xdb";
        let data = std::fs::read(path)?;
        let version = detect_version(&data)?;
        assert_eq!(version, IpVersion::V6);
        Ok(())
    }
}