Skip to main content

ip2location/
common.rs

1use crate::{
2    error::Error,
3    ip2location::{db::LocationDB, record::LocationRecord},
4    ip2proxy::{db::ProxyDB, record::ProxyRecord},
5};
6use memmap2::Mmap;
7use std::{
8    borrow::Cow,
9    net::{IpAddr, Ipv6Addr},
10    path::{Path, PathBuf},
11};
12
13/// Start of the 6to4 IPv6 address range (`2002::/16`).
14pub const FROM_6TO4: u128 = 0x2002_0000_0000_0000_0000_0000_0000_0000;
15/// End of the 6to4 IPv6 address range.
16pub const TO_6TO4: u128 = 0x2002_ffff_ffff_ffff_ffff_ffff_ffff_ffff;
17/// Start of the Teredo IPv6 address range (`2001:0000::/32`).
18pub const FROM_TEREDO: u128 = 0x2001_0000_0000_0000_0000_0000_0000_0000;
19/// End of the Teredo IPv6 address range.
20pub const TO_TEREDO: u128 = 0x2001_0000_ffff_ffff_ffff_ffff_ffff_ffff;
21
22/// A loaded IP2Location or IP2Proxy database.
23///
24/// Created via [`DB::from_file`]. The underlying BIN file is memory-mapped
25/// and remains mapped for the lifetime of this value.
26#[derive(Debug)]
27pub enum DB {
28    /// An IP2Location geolocation database.
29    LocationDb(LocationDB),
30    /// An IP2Proxy proxy-detection database.
31    ProxyDb(ProxyDB),
32}
33
34/// A lookup result from either database type.
35///
36/// The record borrows string data from the memory-mapped file, so it
37/// cannot outlive the [`DB`] that produced it.
38#[derive(Debug)]
39pub enum Record<'a> {
40    /// Geolocation record (country, city, coordinates, …).
41    LocationDb(Box<LocationRecord<'a>>),
42    /// Proxy detection record (proxy type, threat, provider, …).
43    ProxyDb(Box<ProxyRecord<'a>>),
44}
45
46/// Memory-mapped BIN file backing all read operations.
47///
48/// All `read_*` methods use **1-based offsets** to match the IP2Location
49/// BIN format specification. Bounds are checked on every access.
50#[derive(Debug)]
51pub(crate) struct Source {
52    path: PathBuf,
53    map: Mmap,
54}
55
56impl std::fmt::Display for Source {
57    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
58        write!(f, "{}", self.path.display())
59    }
60}
61
62impl Source {
63    /// Wrap an already-mapped file.
64    pub fn new(path: PathBuf, map: Mmap) -> Self {
65        Self { path, map }
66    }
67
68    /// Read a single byte at the given 1-based offset.
69    pub fn read_u8(&self, offset: u64) -> Result<u8, Error> {
70        if offset == 0 {
71            return Err(Error::GenericError("read_u8: offset must be >= 1".into()));
72        }
73        let idx = (offset - 1) as usize;
74        self.map.get(idx).copied().ok_or_else(|| {
75            Error::GenericError(format!("read_u8: offset {} out of bounds (len={})", offset, self.map.len()))
76        })
77    }
78
79    /// Read a little-endian `u32` at the given 1-based offset.
80    pub fn read_u32(&self, offset: u64) -> Result<u32, Error> {
81        if offset == 0 {
82            return Err(Error::GenericError("read_u32: offset must be >= 1".into()));
83        }
84        let start = (offset - 1) as usize;
85        let end = start + 4;
86        let slice = self.map.get(start..end).ok_or_else(|| {
87            Error::GenericError(format!("read_u32: offset {} out of bounds (len={})", offset, self.map.len()))
88        })?;
89        Ok(u32::from_le_bytes(slice.try_into()?))
90    }
91
92    /// Read a little-endian `f32` at the given 1-based offset.
93    pub fn read_f32(&self, offset: u64) -> Result<f32, Error> {
94        if offset == 0 {
95            return Err(Error::GenericError("read_f32: offset must be >= 1".into()));
96        }
97        let start = (offset - 1) as usize;
98        let end = start + 4;
99        let slice = self.map.get(start..end).ok_or_else(|| {
100            Error::GenericError(format!("read_f32: offset {} out of bounds (len={})", offset, self.map.len()))
101        })?;
102        Ok(f32::from_le_bytes(slice.try_into()?))
103    }
104
105    /// Read a length-prefixed string at the given 1-based offset.
106    ///
107    /// The first byte at `offset + 1` is the string length, followed by
108    /// that many bytes of content. Returns `Cow::Borrowed` when the bytes
109    /// are valid UTF-8 (zero-copy), or `Cow::Owned` with lossy replacement.
110    pub fn read_str(&self, offset: u64) -> Result<Cow<'_, str>, Error> {
111        let len = self.read_u8(offset + 1)? as usize;
112        let start = (offset + 1) as usize;
113        let end = start + len;
114        if end > self.map.len() {
115            return Err(Error::GenericError(format!(
116                "read_str: range {}..{} out of bounds (len={})", start, end, self.map.len()
117            )));
118        }
119        let s = String::from_utf8_lossy(&self.map[start..end]);
120        Ok(s)
121    }
122
123    /// Read a 128-bit IPv6 address stored in reverse byte order at the
124    /// given 1-based offset.
125    pub fn read_ipv6(&self, offset: u64) -> Result<Ipv6Addr, Error> {
126        if offset == 0 {
127            return Err(Error::GenericError("read_ipv6: offset must be >= 1".into()));
128        }
129        let start = (offset - 1) as usize;
130        let end = start + 16;
131        if end > self.map.len() {
132            return Err(Error::GenericError(format!(
133                "read_ipv6: range {}..{} out of bounds (len={})", start, end, self.map.len()
134            )));
135        }
136        let mut buf = [0_u8; 16];
137        for i in 0..16 {
138            buf[i] = self.map[start + 15 - i];
139        }
140        Ok(Ipv6Addr::from(buf))
141    }
142}
143
144impl DB {
145    /// Consume the unopened db and mmap the file.
146    pub fn from_file<P: AsRef<Path>>(path: P) -> Result<DB, Error> {
147        //! Loads a Ip2Location/Ip2Proxy Database .bin file from path using
148        //! mmap (memap) feature.
149        //!
150        //! ## Example usage
151        //!
152        //!```rust
153        //! use ip2location::DB;
154        //!
155        //! let mut db = DB::from_file("data/IP2PROXY-IP-COUNTRY.BIN").unwrap();
156        //!```
157        if !path.as_ref().exists() {
158            return Err(Error::IoError(
159                "Error opening DB file: No such file or directory".to_string(),
160            ));
161        }
162
163        let file = std::fs::File::open(&path)?;
164        // SAFETY: The file is opened read-only and we do not modify the
165        // mapped region. The caller must ensure the file is not truncated
166        // while the DB is in use (standard mmap contract).
167        let map = unsafe { Mmap::map(&file) }?;
168
169        // Read product_code (byte 30, 1-indexed) to determine DB type
170        if map.len() < 32 {
171            return Err(Error::GenericError("DB file too small to contain a valid header".into()));
172        }
173        let product_code = map[29]; // byte 30, 0-indexed
174
175        let source = Source::new(path.as_ref().to_path_buf(), map);
176
177        match product_code {
178            1 => {
179                // IP2Location DB
180                let mut ldb = LocationDB::new(source);
181                ldb.read_header()?;
182                Ok(DB::LocationDb(ldb))
183            }
184            2 => {
185                // IP2Proxy DB
186                let mut pdb = ProxyDB::new(source);
187                pdb.read_header()?;
188                Ok(DB::ProxyDb(pdb))
189            }
190            0 => {
191                // Legacy DBs (product_code == 0): try Location first, then Proxy
192                let mut ldb = LocationDB::new(source);
193                match ldb.read_header() {
194                    Ok(()) => Ok(DB::LocationDb(ldb)),
195                    Err(_) => {
196                        // Re-open and try as Proxy
197                        let file = std::fs::File::open(&path)?;
198                        // SAFETY: same contract as above.
199                        let map = unsafe { Mmap::map(&file) }?;
200                        let source = Source::new(path.as_ref().to_path_buf(), map);
201                        let mut pdb = ProxyDB::new(source);
202                        pdb.read_header()?;
203                        Ok(DB::ProxyDb(pdb))
204                    }
205                }
206            }
207            _ => Err(Error::UnknownDb),
208        }
209    }
210
211    pub fn print_db_info(&self) {
212        //! Prints the DB Information of Ip2Location/Ip2Proxy to console
213        //!
214        //! ## Example usage
215        //!
216        //! ```rust
217        //! use ip2location::DB;
218        //!
219        //! let mut db = DB::from_file("data/IP2LOCATION-LITE-DB1.BIN").unwrap();
220        //! db.print_db_info();
221        //! ```
222        match self {
223            Self::LocationDb(db) => db.print_db_info(),
224            Self::ProxyDb(db) => db.print_db_info(),
225        }
226    }
227
228    pub fn ip_lookup(&self, ip: IpAddr) -> Result<Record<'_>, Error> {
229        //! Lookup for the given IPv4 or IPv6 and returns the
230        //! Geo information or Proxy Information
231        //!
232        //! ## Example usage
233        //!
234        //!```rust
235        //! use ip2location::{DB, Record};
236        //!
237        //! let mut db = DB::from_file("data/IP2LOCATION-LITE-DB1.IPV6.BIN").unwrap();
238        //! let geo_info = db.ip_lookup("2a01:cb08:8d14::".parse().unwrap()).unwrap();
239        //! println!("{:#?}", geo_info);
240        //! let record = if let Record::LocationDb(rec) = geo_info {
241        //!   Some(rec)
242        //! } else { None };
243        //! let geo_info = record.unwrap();
244        //! assert!(!geo_info.country.is_none());
245        //! assert_eq!(geo_info.country.unwrap().short_name, "FR")
246        //!```
247        match self {
248            Self::LocationDb(db) => Ok(Record::LocationDb(Box::new(db.ip_lookup(ip)?))),
249            Self::ProxyDb(db) => Ok(Record::ProxyDb(Box::new(db.ip_lookup(ip)?))),
250        }
251    }
252}