Skip to main content

xdb_parse/
xdb.rs

1//! Xdb file metadata: header parsing and IP version detection.
2
3use crate::error::XdbError;
4use crate::{IPV4_SEGMENT_INDEX_BLOCK_SIZE, IPV6_SEGMENT_INDEX_BLOCK_SIZE};
5
6/// IP protocol version of an xdb database.
7///
8/// Returned by [`detect_version`] and [`Header::ip_version`].
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum IpVersion {
11    /// IPv4 xdb — segments are 14 bytes each.
12    V4,
13    /// IPv6 xdb — segments are 38 bytes each.
14    V6,
15}
16
17/// Parsed xdb file header (first 16 bytes of meaningful data in the 256-byte header block).
18///
19/// # Fields
20///
21/// - `version` — xdb format version (typically 2)
22/// - `cache_type` — cache strategy hint
23/// - `generation_time` — Unix timestamp of database generation
24/// - `index_base_address` — file offset where segment index starts
25/// - `index_end_address` — file offset where segment index ends (inclusive)
26///
27/// # Example
28///
29/// ```no_run
30/// let data = xdb_parse::load_file("./assets/ip2region_v4.xdb".into())?;
31/// let header = xdb_parse::Header::parse(&data)?;
32/// println!("xdb v{}, generated at {}", header.version, header.generation_time);
33/// # Ok::<(), xdb_parse::error::XdbError>(())
34/// ```
35#[derive(Debug, Clone, Default)]
36pub struct Header {
37    pub version: u16,
38    pub cache_type: u16,
39    pub generation_time: u32,
40    pub index_base_address: u32,
41    pub index_end_address: u32,
42}
43
44impl Header {
45    /// Parse the header from the first bytes of an xdb file.
46    ///
47    /// Reads fields from the header block (offsets 0..16 within the 256-byte header).
48    pub fn parse(data: &[u8]) -> Result<Self, XdbError> {
49        Ok(Header {
50            version: u16::from_ne_bytes(data[0..2].try_into()?),
51            cache_type: u16::from_ne_bytes(data[2..4].try_into()?),
52            generation_time: u32::from_ne_bytes(data[4..8].try_into()?),
53            index_base_address: u32::from_ne_bytes(data[8..12].try_into()?),
54            index_end_address: u32::from_ne_bytes(data[12..16].try_into()?),
55        })
56    }
57
58    /// Detect the IP version by examining the segment index size.
59    ///
60    /// Uses the segment index range (`index_end_address - index_base_address`)
61    /// to determine whether segments are 14 bytes (IPv4) or 38 bytes (IPv6).
62    pub fn ip_version(&self) -> Result<IpVersion, XdbError> {
63        let len = self.index_end_address - self.index_base_address + 1;
64        if len % IPV4_SEGMENT_INDEX_BLOCK_SIZE <= 1 {
65            Ok(IpVersion::V4)
66        } else if len % IPV6_SEGMENT_INDEX_BLOCK_SIZE <= 1 {
67            Ok(IpVersion::V6)
68        } else {
69            Err(XdbError::InvalidIPVersion("unknown xdb version".into()))
70        }
71    }
72}
73
74/// Detect the IP version from loaded xdb data.
75///
76/// Parses the header and calls [`Header::ip_version`]. This is a convenience
77/// function — use [`Header::parse`] directly if you also need other header fields.
78///
79/// # Example
80///
81/// ```no_run
82/// let data = xdb_parse::load_file("./assets/ip2region_v4.xdb".into())?;
83/// let version = xdb_parse::detect_version(&data)?;
84/// assert_eq!(version, xdb_parse::IpVersion::V4);
85/// # Ok::<(), xdb_parse::error::XdbError>(())
86/// ```
87pub fn detect_version(data: &[u8]) -> Result<IpVersion, XdbError> {
88    Header::parse(data)?.ip_version()
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94    use anyhow::Result;
95
96    #[test]
97    fn test_parse_header() -> Result<()> {
98        let mut buf = vec![0u8; 256];
99        buf[0..2].copy_from_slice(&2u16.to_ne_bytes());
100        let header = Header::parse(&buf)?;
101        assert_eq!(header.version, 2);
102        Ok(())
103    }
104
105    #[test]
106    fn test_detect_version_v4() -> Result<()> {
107        let path = "./assets/ip2region_v4.xdb";
108        let data = std::fs::read(path)?;
109        let version = detect_version(&data)?;
110        assert_eq!(version, IpVersion::V4);
111        Ok(())
112    }
113
114    #[test]
115    fn test_detect_version_v6() -> Result<()> {
116        let path = "./assets/ip2region_v6.xdb";
117        let data = std::fs::read(path)?;
118        let version = detect_version(&data)?;
119        assert_eq!(version, IpVersion::V6);
120        Ok(())
121    }
122}