From df6004b42e70c1debe02f5a4548eb7d47745e59b Mon Sep 17 00:00:00 2001 From: gongzhengyang Date: Sun, 28 Sep 2025 15:20:37 +0800 Subject: [PATCH] Feat: add rust maker for ipv4 and ipv6 --- binding/rust/ReadMe.md | 2 + binding/rust/ip2region/Cargo.toml | 3 +- binding/rust/ip2region/src/error.rs | 6 +- binding/rust/ip2region/src/header.rs | 86 --------- binding/rust/ip2region/src/lib.rs | 3 +- binding/rust/ip2region/src/searcher.rs | 9 +- maker/rust/README.md | 85 ++++++--- maker/rust/maker/Cargo.toml | 18 +- maker/rust/maker/src/command.rs | 22 +++ maker/rust/maker/src/error.rs | 37 ++++ maker/rust/maker/src/header.rs | 165 ++++++++++++++++++ maker/rust/maker/src/ip_value.rs | 61 ------- maker/rust/maker/src/lib.rs | 13 ++ maker/rust/maker/src/main.rs | 230 +++---------------------- maker/rust/maker/src/maker.rs | 131 ++++++++++++++ maker/rust/maker/src/segment.rs | 166 ++++++++++++++++++ maker/rust/vimdiff.png | Bin 21382 -> 0 bytes 17 files changed, 639 insertions(+), 398 deletions(-) delete mode 100644 binding/rust/ip2region/src/header.rs create mode 100644 maker/rust/maker/src/command.rs create mode 100644 maker/rust/maker/src/error.rs create mode 100644 maker/rust/maker/src/header.rs delete mode 100644 maker/rust/maker/src/ip_value.rs create mode 100644 maker/rust/maker/src/lib.rs create mode 100644 maker/rust/maker/src/maker.rs create mode 100644 maker/rust/maker/src/segment.rs delete mode 100644 maker/rust/vimdiff.png diff --git a/binding/rust/ReadMe.md b/binding/rust/ReadMe.md index 430c61c..9832582 100644 --- a/binding/rust/ReadMe.md +++ b/binding/rust/ReadMe.md @@ -123,6 +123,7 @@ $ cargo build -r #### 测试 IPv6 ```bash $ cd binding/rust +$ cargo build -r $ ./target/release/searcher --xdb='../../data/ip2region_v6.xdb' query ip2region xdb searcher test program, type `quit` or `Ctrl + c` to exit @@ -140,6 +141,7 @@ ip2region>> #### 测试 IPv4 ```bash $ cd binding/rust +$ cargo build -r $ ./target/release/searcher --xdb='../../data/ip2region_v4.xdb' query ip2region xdb searcher test program, type `quit` or `Ctrl + c` to exit ip2region>> 1.1.2.1 diff --git a/binding/rust/ip2region/Cargo.toml b/binding/rust/ip2region/Cargo.toml index a97a888..cc60b34 100644 --- a/binding/rust/ip2region/Cargo.toml +++ b/binding/rust/ip2region/Cargo.toml @@ -11,8 +11,7 @@ license = "Apache-2.0" [dependencies] tracing = "0.1" thiserror = "2" -num-traits = "0.2" -num-derive = "0.4" +maker = { path = "../../../maker/rust/maker"} [dev-dependencies] criterion = "0.7" diff --git a/binding/rust/ip2region/src/error.rs b/binding/rust/ip2region/src/error.rs index abc3c61..be4ae85 100644 --- a/binding/rust/ip2region/src/error.rs +++ b/binding/rust/ip2region/src/error.rs @@ -12,9 +12,6 @@ pub enum Ip2RegionError { #[error("No matched Ipaddress")] NoMatchedIP, - #[error("Header parse error: {0}")] - HeaderParsed(String), - #[error("Searcher load IPv4 data, couldn't search IPv6 data")] OnlyIPv4Version, @@ -23,6 +20,9 @@ pub enum Ip2RegionError { #[error("Try from slice failed")] TryFromSliceFailed(#[from] std::array::TryFromSliceError), + + #[error("Maker crate error: {0}")] + MakerError(#[from] maker::MakerError), } pub type Result = std::result::Result; diff --git a/binding/rust/ip2region/src/header.rs b/binding/rust/ip2region/src/header.rs deleted file mode 100644 index d9c0dc8..0000000 --- a/binding/rust/ip2region/src/header.rs +++ /dev/null @@ -1,86 +0,0 @@ -use num_derive::FromPrimitive; -use num_traits::FromPrimitive; - -use crate::error::Ip2RegionError; - -pub const HEADER_INFO_LENGTH: usize = 256; - -#[allow(dead_code)] -#[derive(Debug)] -pub struct Header { - version: u16, - index_policy: IndexPolicy, - create_time: u32, - start_index_ptr: u32, - end_index_ptr: u32, - ip_version: IpVersion, - runtime_ptr_bytes: u16, -} - -impl TryFrom<&[u8; 256]> for Header { - type Error = Ip2RegionError; - - fn try_from(value: &[u8; 256]) -> Result { - if value.len() < 20 { - return Err(Ip2RegionError::HeaderParsed( - "Header bytes too short".into(), - )); - } - - let index_policy_value = u16::from_le_bytes([value[2], value[3]]); - let ip_version_value = u16::from_le_bytes([value[16], value[17]]); - - Ok(Header { - version: u16::from_le_bytes([value[0], value[1]]), - index_policy: IndexPolicy::from_u16(index_policy_value).ok_or_else(|| { - Ip2RegionError::HeaderParsed(format!( - "Header index policy invalid: {index_policy_value}" - )) - })?, - create_time: u32::from_le_bytes([value[4], value[5], value[6], value[7]]), - start_index_ptr: u32::from_le_bytes([value[8], value[9], value[10], value[11]]), - end_index_ptr: u32::from_le_bytes([value[12], value[13], value[14], value[15]]), - - ip_version: IpVersion::from_u16(ip_version_value).ok_or_else(|| { - Ip2RegionError::HeaderParsed(format!( - "Header ip version invalid: {ip_version_value}" - )) - })?, - runtime_ptr_bytes: u16::from_le_bytes([value[18], value[19]]), - }) - } -} - -#[derive(FromPrimitive, Debug)] -#[repr(u16)] -pub enum IndexPolicy { - VectorIndex = 1, - BTreeIndex = 2, -} - -#[derive(FromPrimitive, Debug)] -#[repr(u16)] -pub enum IpVersion { - V4 = 4, - V6 = 6, -} - -impl Header { - pub fn ip_bytes_len(&self) -> usize { - match &self.ip_version { - IpVersion::V4 => 4, - IpVersion::V6 => 16, - } - } - - pub fn segment_index_size(&self) -> usize { - match &self.ip_version { - IpVersion::V4 => 14, - IpVersion::V6 => 38, - } - } - - pub fn ip_version(&self) -> &IpVersion { - &self.ip_version - } -} diff --git a/binding/rust/ip2region/src/lib.rs b/binding/rust/ip2region/src/lib.rs index f892edc..e51adb7 100644 --- a/binding/rust/ip2region/src/lib.rs +++ b/binding/rust/ip2region/src/lib.rs @@ -1,7 +1,6 @@ mod error; -mod header; mod ip_value; mod searcher; pub use searcher::{CachePolicy, Searcher}; -pub use ip_value::IpValueExt; +pub use ip_value::IpValueExt; \ No newline at end of file diff --git a/binding/rust/ip2region/src/searcher.rs b/binding/rust/ip2region/src/searcher.rs index 4694917..91917ee 100644 --- a/binding/rust/ip2region/src/searcher.rs +++ b/binding/rust/ip2region/src/searcher.rs @@ -9,13 +9,12 @@ use std::sync::OnceLock; use tracing::{debug, trace, warn}; use crate::error::{Ip2RegionError, Result}; -use crate::header::{HEADER_INFO_LENGTH, Header, IpVersion}; +use maker::{ + HEADER_INFO_LENGTH, Header, IpVersion, VECTOR_INDEX_LENGTH, VECTOR_INDEX_COLS, + VECTOR_INDEX_SIZE +}; use crate::ip_value::{CompareExt, IpValueExt}; -const VECTOR_INDEX_LENGTH: usize = 256 * 256 * 8; -const VECTOR_INDEX_COLS: usize = 256; -const VECTOR_INDEX_SIZE: usize = 8; - pub struct Searcher { pub filepath: String, pub cache_policy: CachePolicy, diff --git a/maker/rust/README.md b/maker/rust/README.md index 43da538..9c33d8c 100644 --- a/maker/rust/README.md +++ b/maker/rust/README.md @@ -1,32 +1,71 @@ # ip2region xdb rust 生成实现 -## 使用方法 -* 当前目录下maker子目录下执行 `cargo build --release` 编译生成工具 -* 目标生成在targe/release 目录下 maker -* 使用方法: +## 程序编译 +```bash +$ cd maker/rust/maker +$ cargo build -r ``` - Usage: maker --in-file --out-file - Options: - -i, --in-file - -o, --out-file - -h, --help Print help - -V, --version Print version +编译成功以后,执行文件位置 `./target/release/maker` + +## `xdb` 数据生成 +```bash +# CWD ip2region/maker/rust/maker +$ ./target/release/maker --help +Usage: maker [OPTIONS] --src --dst --ip-version + +Options: + --src + ip source region txt filepath + + --dst + generated xdb filepath + + --ip-version + Possible values: + - v4: IPv4 + - v6: Ipv6 + + --index-policy + index cache policy + + [default: vector-index] + [possible values: vector-index, b-tree-index] + + --filter-fields + region filter fields, the index of the fields, e.g. `1,2,3,5` + + -h, --help + Print help (see a summary with '-h') ``` -例如,通过默认的 data/ip.merge.txt 原数据,在target目录下生成一个 ip2region.xdb 二进制文件: - -``` -kevin@ubuntu ~/i/m/r/m/t/release (master)> ./maker -i ../../../../../data/ip.merge.txt -o ip2region.xdb -load 683844 lines -try to write the segment index ptr ... -write done, dataBlocks: 13827, IndexBlock: 683844, indexPtr: (983587, 11070069) -Done, elpsed: 0m7s - +例如,使用默认的仓库 data/ 下默认的原始数据生成生成 xdb 文件到当前目录(`ip2region/maker/rust/maker`): +```bash +# ipv6 +./target/release/maker --src=../../../data/ipv6_source.txt --dst=./target/ipv6.xdb --ip-version v6 +# ipv4 +./target/release/maker --src=../../../data/ipv4_source.txt --dst=./target/ipv4.xdb --ip-version v4 ``` -## 数据查询/bench 测试 -* 通过将以上步骤生成的二进制文件和python 生成工具生成的二进制文件进行二进制比对,除时间戳位置不同,其它均相同。 +## `xdb` 数据查询 和 bench 测试 -![](./vimdiff.png) -* 已经完成开发的 [binding](../../binding/) 都有查询和 bench 测试程序以及使用文档,你可以使用你熟悉的语言的 searcher 进行查询测试或者bench测试,来确认数据的正确性和完整性。 +基于xdb 格式的查询功能和测试见 [ip2region binding](../../binding) +## 对比其他 maker 生成的 xdb 文件 +推荐使用 `vbindiff`, 与其他文件的差异只有 create time 信息上有差异,其他数据都需要是一样的 + +golang 版本 maker 构建 xdb +```bash +$ cd maker golang +$ make +$ ./xdb_maker gen --src=../../data/ipv4_source.txt --dst=./ip2region_v4.xdb --version=ipv4 +$ ./xdb_maker gen --src=../../data/ipv6_source.txt --dst=./ip2region_v6.xdb --version=ipv6 +``` + +对比 xdb 差异 +```bash +$ cd maker/rust/maker +$ ./target/release/maker --src=../../../data/ipv4_source.txt --dst=./target/ipv4.xdb --ip-version v4 +$ ./target/release/maker --src=../../../data/ipv6_source.txt --dst=./target/ipv6.xdb --ip-version v6 +$ vbindiff ./ipv4.xdb ../../golang/ip2region_v4.xdb +$ vbindiff ./ipv6.xdb ../../golang/ip2region_v6.xdb +``` diff --git a/maker/rust/maker/Cargo.toml b/maker/rust/maker/Cargo.toml index 19d6000..34d0634 100644 --- a/maker/rust/maker/Cargo.toml +++ b/maker/rust/maker/Cargo.toml @@ -1,12 +1,18 @@ [package] name = "maker" -version = "0.1.0" -edition = "2021" -authors = ["Kevin Wang "] +version = "0.2.0" +edition = "2024" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -clap = { version = "4.4.18", features = ["derive"] } -lazy_static = "1.4.0" -once_cell = "1.19.0" +clap = { version = "4.5", features = ["derive"] } +thiserror = "2" +bytes = "1" +num-derive = "0.4.2" +num-traits = "0.2.19" +chrono = "0.4" +itertools = "0.14" +ipnetwork = "0.21" +tracing = "0.1" +tracing-subscriber = "0.3" diff --git a/maker/rust/maker/src/command.rs b/maker/rust/maker/src/command.rs new file mode 100644 index 0000000..a5bf47a --- /dev/null +++ b/maker/rust/maker/src/command.rs @@ -0,0 +1,22 @@ +use clap::Parser; + +use crate::IpVersion; +use crate::header::IndexPolicy; + +#[derive(Parser, Debug)] +pub struct Command { + /// ip source region txt filepath + #[arg(long)] + pub src: String, + /// generated xdb filepath + #[clap(long)] + pub dst: String, + #[clap(long, value_enum)] + pub ip_version: IpVersion, + /// index cache policy + #[clap(long, value_enum, default_value_t = IndexPolicy::VectorIndex)] + pub index_policy: IndexPolicy, + /// region filter fields, the index of the fields, e.g. `1,2,3,5` + #[clap(long, value_delimiter = ',')] + pub filter_fields: Vec, +} diff --git a/maker/rust/maker/src/error.rs b/maker/rust/maker/src/error.rs new file mode 100644 index 0000000..3d05c6b --- /dev/null +++ b/maker/rust/maker/src/error.rs @@ -0,0 +1,37 @@ +#[derive(Debug, thiserror::Error)] +pub enum MakerError { + #[error("Io error: {0}")] + IoError(#[from] std::io::Error), + + #[error("Header parse error: {0}")] + HeaderParsed(String), + + #[error("Parse line src ip, dst ip, region failed for line: {0}")] + ParseIPRegion(String), + + #[error("Invalid sip/eip version")] + InvalidIPVersion, + + #[error("Ipaddr parse error: {0}")] + IpaddrParseError(#[from] std::net::AddrParseError), + + #[error("Region filter fields value too big, limit: {limit}, actual: {actual}")] + RegionFilterFieldsTooBig { limit: usize, actual: usize }, + + #[error("Empty segments")] + EmptySegments, + + #[error("Try from int failed")] + TryFromIntError(#[from] std::num::TryFromIntError), + + #[error("Invalid IP Network")] + InvalidIPNetwork(#[from] ipnetwork::IpNetworkError), + + #[error("Try from slice failed")] + TryFromSliceFailed(#[from] std::array::TryFromSliceError), + + #[error("Region could not found")] + RegionNotFound, +} + +pub type Result = std::result::Result; diff --git a/maker/rust/maker/src/header.rs b/maker/rust/maker/src/header.rs new file mode 100644 index 0000000..7b8b1d0 --- /dev/null +++ b/maker/rust/maker/src/header.rs @@ -0,0 +1,165 @@ +use std::fmt::Display; +use std::net::IpAddr; + +use bytes::{BufMut, Bytes, BytesMut}; +use clap::ValueEnum; +use itertools::Itertools; +use num_derive::FromPrimitive; +use num_traits::FromPrimitive; + +use crate::error::{MakerError, Result}; + +pub const VERSION_NO: u16 = 3; // since 2025/09/01 (IPv6 supporting) +pub const HEADER_INFO_LENGTH: usize = 256; +pub const VECTOR_INDEX_COLS: usize = 256; +pub const VECTOR_INDEX_ROWS: usize = 256; +pub const VECTOR_INDEX_SIZE: usize = 8; +pub const VECTOR_INDEX_LENGTH: usize = VECTOR_INDEX_COLS * VECTOR_INDEX_ROWS * VECTOR_INDEX_SIZE; +pub const RUNTIME_PTR_SIZE: u16 = 4; +pub const REGION_START: u64 = (HEADER_INFO_LENGTH + VECTOR_INDEX_LENGTH) as u64; + +#[allow(dead_code)] +#[derive(Debug)] +pub struct Header { + version: u16, + index_policy: IndexPolicy, + create_time: u32, + start_index_ptr: u32, + end_index_ptr: u32, + ip_version: IpVersion, + runtime_ptr_bytes: u16, +} + +impl TryFrom<&[u8; 256]> for Header { + type Error = MakerError; + + fn try_from(value: &[u8; 256]) -> Result { + if value.len() < 20 { + return Err(MakerError::HeaderParsed("Header bytes too short".into())); + } + + let index_policy_value = u16::from_le_bytes([value[2], value[3]]); + let ip_version_value = u16::from_le_bytes([value[16], value[17]]); + + Ok(Header { + version: u16::from_le_bytes([value[0], value[1]]), + index_policy: IndexPolicy::from_u16(index_policy_value).ok_or_else(|| { + MakerError::HeaderParsed(format!( + "Header index policy invalid: {index_policy_value}" + )) + })?, + create_time: u32::from_le_bytes([value[4], value[5], value[6], value[7]]), + start_index_ptr: u32::from_le_bytes([value[8], value[9], value[10], value[11]]), + end_index_ptr: u32::from_le_bytes([value[12], value[13], value[14], value[15]]), + + ip_version: IpVersion::from_u16(ip_version_value).ok_or_else(|| { + MakerError::HeaderParsed(format!("Header ip version invalid: {ip_version_value}")) + })?, + runtime_ptr_bytes: u16::from_le_bytes([value[18], value[19]]), + }) + } +} + +impl Header { + pub fn new(index_policy: IndexPolicy, ip_version: IpVersion) -> Header { + Header { + version: VERSION_NO, + index_policy, + create_time: chrono::Utc::now().timestamp() as u32, + start_index_ptr: 0, + end_index_ptr: 0, + ip_version, + runtime_ptr_bytes: RUNTIME_PTR_SIZE, + } + } + + pub fn encode_bytes(&self, start_index_ptr: u32, end_index_ptr: u32) -> Bytes { + let mut buf = BytesMut::with_capacity(HEADER_INFO_LENGTH); + buf.put_u16_le(VERSION_NO); + buf.put_u16_le(self.index_policy as u16); + buf.put_u32_le(self.create_time); + buf.put_u32_le(start_index_ptr); + // index block end ptr + buf.put_u32_le(end_index_ptr); + buf.put_u16_le(self.ip_version as u16); + buf.put_u16_le(self.runtime_ptr_bytes); + buf.freeze() + } +} + +#[derive(FromPrimitive, Debug, Copy, Clone, ValueEnum)] +#[repr(u16)] +pub enum IndexPolicy { + VectorIndex = 1, + BTreeIndex = 2, +} + +impl Display for IndexPolicy { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + IndexPolicy::VectorIndex => write!(f, "VectorIndex"), + IndexPolicy::BTreeIndex => write!(f, "BTreeIndex"), + } + } +} + +#[derive(FromPrimitive, Debug, Copy, Clone, ValueEnum, PartialEq)] +#[repr(u16)] +pub enum IpVersion { + /// IPv4 + V4 = 4, + /// Ipv6 + V6 = 6, +} + +impl IpVersion { + pub fn ip_bytes_len(&self) -> usize { + match &self { + IpVersion::V4 => 4, + IpVersion::V6 => 16, + } + } + + pub fn segment_index_size(&self) -> usize { + match &self { + IpVersion::V4 => 14, + IpVersion::V6 => 38, + } + } +} + +impl Header { + pub fn ip_bytes_len(&self) -> usize { + self.ip_version.ip_bytes_len() + } + + pub fn segment_index_size(&self) -> usize { + self.ip_version.segment_index_size() + } + + pub fn ip_version(&self) -> &IpVersion { + &self.ip_version + } +} + +pub trait IPAddrExt { + fn ipaddr_bytes(&self) -> Vec; + + fn encode_ipaddr_bytes(&self) -> Vec; +} + +impl IPAddrExt for IpAddr { + fn ipaddr_bytes(&self) -> Vec { + match self { + IpAddr::V4(addr) => addr.octets().to_vec(), + IpAddr::V6(addr) => addr.octets().to_vec(), + } + } + + fn encode_ipaddr_bytes(&self) -> Vec { + match self { + IpAddr::V4(addr) => addr.octets().into_iter().rev().collect_vec(), + IpAddr::V6(addr) => addr.octets().to_vec(), + } + } +} diff --git a/maker/rust/maker/src/ip_value.rs b/maker/rust/maker/src/ip_value.rs deleted file mode 100644 index 0a11f92..0000000 --- a/maker/rust/maker/src/ip_value.rs +++ /dev/null @@ -1,61 +0,0 @@ -use std::error::Error; -use std::net::Ipv4Addr; -use std::str::FromStr; - -pub trait ToUIntIP { - fn to_u32_ip(&self) -> Result>; -} - -impl ToUIntIP for u32 { - fn to_u32_ip(&self) -> Result> { - Ok(self.to_owned()) - } -} - -impl ToUIntIP for &str { - fn to_u32_ip(&self) -> Result> { - if let Ok(ip_addr) = Ipv4Addr::from_str(self) { - return Ok(u32::from(ip_addr)); - } - Ok(self.parse::()?) - } -} - -impl ToUIntIP for Ipv4Addr { - fn to_u32_ip(&self) -> Result> { - Ok(u32::from(*self)) - } -} - -#[cfg(test)] -mod test_ip { - use super::*; - - #[test] - fn test_ip_str_2_u32() { - let ip_str = "1.1.1.1"; - let result = ip_str.to_u32_ip().unwrap(); - assert_eq!(result, 1 << 24 | 1 << 16 | 1 << 8 | 1); - } - - #[test] - fn test_ip_u32_str() { - let ip = "12"; - let result = ip.to_u32_ip().unwrap(); - assert_eq!(result, 12); - } - - #[test] - fn test_ip_u32() { - let ip: u32 = 33; - let result = ip.to_u32_ip().unwrap(); - assert_eq!(result, 33); - } - - #[test] - fn test_ip_addr() { - let ip = Ipv4Addr::from_str("0.0.3.12").unwrap(); - let result = ip.to_u32_ip().unwrap(); - assert_eq!(result, 3 << 8 | 12) - } -} diff --git a/maker/rust/maker/src/lib.rs b/maker/rust/maker/src/lib.rs new file mode 100644 index 0000000..1866d55 --- /dev/null +++ b/maker/rust/maker/src/lib.rs @@ -0,0 +1,13 @@ +mod command; +mod error; +mod header; +mod maker; +mod segment; + +pub use command::Command; +pub use error::{MakerError, Result}; +pub use header::{ + HEADER_INFO_LENGTH, Header, IpVersion, REGION_START, VECTOR_INDEX_COLS, VECTOR_INDEX_LENGTH, + VECTOR_INDEX_SIZE, +}; +pub use maker::Maker; diff --git a/maker/rust/maker/src/main.rs b/maker/rust/maker/src/main.rs index 3318503..f8fb642 100644 --- a/maker/rust/maker/src/main.rs +++ b/maker/rust/maker/src/main.rs @@ -1,215 +1,25 @@ +use std::time::Instant; + use clap::Parser; -use std::path::PathBuf; -use std::fs::File; -use std::io::{BufReader, BufRead, Error, Write, Seek, SeekFrom}; -use lazy_static::lazy_static; -use std::collections::HashMap; -use std::sync::Mutex; -mod ip_value; -pub use self::ip_value::ToUIntIP; -use std::time::{SystemTime, UNIX_EPOCH, Instant}; +use maker::{Command, Maker, Result}; +use tracing::info; -const HEADER_LEN:u32 = 256; -const VECTOR_INDEX_LEN:u32 = 256*256*8; -const SEGMENT_INDEX_BLOCK_SIZE:u32 = 14; -static mut START_INDEX_POS:u32 = 0; -static mut END_INDEX_POS:u32 = 0; -const PROTOCAL:u16 = 2; -const INDEX_POLICY:u16 = 1; - -lazy_static! { - static ref REG_MAP: Mutex> = Mutex::new(HashMap::new()); -} - -lazy_static! { - static ref V_SEG: Mutex> = Mutex::new(Vec::new()); -} - -#[derive(Parser)] -#[command(author="Kevin Wang ", version="2.0")] -#[command(help_template = " Author: {author-with-newline} {about-section}Version: {version} \n {usage-heading} {usage} \n {all-args} {tab}")] -struct Args { - #[arg(short, long)] - in_file: PathBuf, - #[arg(short, long)] - out_file: PathBuf, -} - -struct Segment { - sip: u32, - eip: u32, - reg: String, -} - -#[derive(Debug, Clone, Copy)] -struct IndexBlock { - first_pos: u32, - last_pos: u32, -} - -impl Segment { - fn new(sip: u32, eip: u32, reg: String) -> Segment { - Segment {sip, eip, reg} - } -} - -fn load_segments(in_file: PathBuf) -> std::io::Result { - let in_f = File::open(in_file)?; - let reader = BufReader::new(in_f); - let mut count = 0; - let last_eip = 0; - for line in reader.lines() { - let line = line?; - let v: Vec<&str> = line.splitn(3, '|').collect(); - if v.len() != 3 { - panic!("invalid ip segment line '{}'", line) - } - let sip = v[0].to_u32_ip().expect(&format!("invalid ip address '{}' in line {}", v[0], line)); - let eip = v[1].to_u32_ip().expect(&format!("invalid ip address '{}' in line {}", v[0], line)); - if sip > eip { - panic!("start ip({}) should not be greater than end ip({})", sip, eip); - } - if v[2].len() < 1 { - panic!("empty region info in segment line `{}`", line); - } - // Check the continuity of data segment - if last_eip != 0 && last_eip + 1 != sip { - panic!("discontinuous data segment: last.eip+1!=seg.sip in line {}", line); - } - let segment = Segment::new(sip, eip, v[2].to_string()); - V_SEG.lock().unwrap().push(segment); - - count += 1; - } - return Ok(count.to_string()); -} - -fn write_region(out_fd: &mut File) -> std::io::Result<()> { - out_fd.seek(SeekFrom::Start((HEADER_LEN + VECTOR_INDEX_LEN).into()))?; - let v_seg = V_SEG.lock().unwrap(); - for seg in v_seg.iter() { - if REG_MAP.lock().unwrap().get(&seg.reg) == None { - let pos = out_fd.stream_position()?; - REG_MAP.lock().unwrap().insert(seg.reg.to_string(), pos as u32); - out_fd.write(seg.reg.as_bytes())?; - } - } - return Ok(()); -} - -fn split_ip(sip: u32, eip: u32, reg: String) -> Vec { - let s1 = sip >> 24 & 0xFF; - let s2 = sip >> 16 & 0xFF; - //let s3 = sip >> 8 & 0xFF; - //let s4 = sip & 0xFF; - - let e1 = eip >> 24 & 0xFF; - let e2 = eip >> 16 & 0xFF; - //let e3 = eip >> 8 & 0xFF; - //let e4 = eip & 0xFF; - - let mut node_list: Vec = Vec::new(); - - // println!("split:{}.{}.{}.{}~{}.{}.{}.{}", s1,s2,s3,s4,e1,e2,e3,e4); - for i in s1..e1+1 { - for j in (if i == s1 {s2} else {0})..(if i == e1 {e2+1} else {256}){ - let s_ip = if i == s1 && j == s2 { - sip - }else { - ((i << 24) & 0xff000000) | (j << 16 & 0xff0000) - }; - let e_ip = if i == e1 && j == e2 { - eip - }else { - ((i << 24) & 0xff000000) | ((j << 16) &0xff0000) | 0xffff - }; - node_list.push(Segment{sip:s_ip, eip:e_ip, reg:reg.to_string()}); - } - } - - return node_list; -} - -fn set_vector_index(arr: &mut [[IndexBlock; 256]; 256], ip:u32, block_pos:u32) { - let row: usize = (ip >> 24 & 0xff) as usize; - let col: usize = (ip >> 16 & 0xff ) as usize; - let vi_block = &mut arr[row][col]; - if vi_block.first_pos == 0 { - vi_block.first_pos = block_pos; - } - vi_block.last_pos = block_pos + SEGMENT_INDEX_BLOCK_SIZE; -} - -fn write_index_block(out_fd: &mut File) -> std::io::Result<()> { - let v_seg = V_SEG.lock().unwrap(); - let mut index_arr :[[IndexBlock; 256]; 256] = [[IndexBlock{first_pos: 0, last_pos: 0}; 256]; 256]; - for seg in v_seg.iter() { - let pos = REG_MAP.lock().unwrap().get(&seg.reg).copied().unwrap(); - let node_list = split_ip(seg.sip, seg.eip, seg.reg.to_string()); - for node in node_list { - let block_pos = out_fd.stream_position()?; - out_fd.write(&node.sip.to_le_bytes())?; - out_fd.write(&node.eip.to_le_bytes())?; - out_fd.write(&(node.reg.len() as u16).to_le_bytes())?; - out_fd.write(&pos.to_le_bytes())?; - set_vector_index(&mut index_arr, node.sip, block_pos as u32); - unsafe { - if START_INDEX_POS == 0 { - START_INDEX_POS = block_pos as u32; - } - END_INDEX_POS = block_pos as u32; - } - } - } - println!("try to write the segment index ptr ..."); - - out_fd.seek(SeekFrom::Start(HEADER_LEN.into()))?; - for i in 0..256 { - for j in 0..256 { - let index = index_arr[i][j]; - out_fd.write(&index.first_pos.to_le_bytes())?; - out_fd.write(&index.last_pos.to_le_bytes())?; - } - } - return Ok(()); -} - -fn write_header(out_fd: &mut File) -> std::io::Result<()> { - out_fd.seek(SeekFrom::Start(0))?; - out_fd.write(&PROTOCAL.to_le_bytes())?; - out_fd.write(&INDEX_POLICY.to_le_bytes())?; - let now = SystemTime::now(); - let timestamp = now.duration_since(UNIX_EPOCH).expect("Time went backwards").as_secs() as u32; - out_fd.write(×tamp.to_le_bytes())?; - unsafe { - out_fd.write(&START_INDEX_POS.to_le_bytes())?; - out_fd.write(&END_INDEX_POS.to_le_bytes())?; - } - - return Ok(()) - -} - -fn main() -> Result<(), Error> { - let args = Args::parse(); +/// Ip2Region database structure +/// See https://github.com/lionsoul2014/ip2region/blob/master/maker/golang/xdb/maker.go +fn main() -> Result<()> { + tracing_subscriber::fmt::init(); let now = Instant::now(); - match load_segments(args.in_file) { - Ok(result) => println!("load {} lines", result), - Err(err) => println!("{}", err), - } - let mut out_fd = File::create(args.out_file).unwrap(); - write_region(&mut out_fd)?; - write_index_block(&mut out_fd)?; - write_header(&mut out_fd)?; - unsafe { - println!("write done, dataBlocks: {}, IndexBlock: {}, indexPtr: ({}, {})", - REG_MAP.lock().unwrap().len(), - V_SEG.lock().unwrap().len(), - START_INDEX_POS, END_INDEX_POS - ); - } - let sec = now.elapsed().as_secs(); + let cmd = Command::parse(); + info!(?cmd, "Generate xdb"); + let mut maker = Maker::new( + cmd.ip_version, + cmd.index_policy, + &cmd.src, + &cmd.dst, + cmd.filter_fields, + )?; + maker.start()?; - println!("Done, elpsed: {}m{}s", sec/60, sec%60); - return Ok(()); + info!(cost_time=?now.elapsed(), "Make completed"); + Ok(()) } diff --git a/maker/rust/maker/src/maker.rs b/maker/rust/maker/src/maker.rs new file mode 100644 index 0000000..541d21a --- /dev/null +++ b/maker/rust/maker/src/maker.rs @@ -0,0 +1,131 @@ +use std::collections::HashMap; +use std::fs::File; +use std::io::{Seek, SeekFrom, Write}; +use std::sync::Arc; + +use bytes::{BufMut, BytesMut}; +use itertools::Itertools; +use tracing::{info, trace}; + +use crate::error::{MakerError, Result}; +use crate::header::{IPAddrExt, IndexPolicy, IpVersion, VECTOR_INDEX_ROWS}; +use crate::segment::Segment; +use crate::{HEADER_INFO_LENGTH, Header, REGION_START, VECTOR_INDEX_COLS, VECTOR_INDEX_SIZE}; + +pub struct Maker { + ip_version: IpVersion, + dst_file: File, + region_pool: HashMap, u32>, + vector_index: [[[u8; VECTOR_INDEX_SIZE]; VECTOR_INDEX_ROWS]; VECTOR_INDEX_COLS], + segments: Vec, + header: Header, +} + +impl Maker { + pub fn new( + ip_version: IpVersion, + index_policy: IndexPolicy, + src_filepath: &str, + end_filepath: &str, + filter_fields: Vec, + ) -> Result { + let header = Header::new(index_policy, ip_version); + + let segments = Segment::from_file(src_filepath, ip_version, &filter_fields)?; + if segments.is_empty() { + return Err(MakerError::EmptySegments); + } + + let mut region_pool = HashMap::with_capacity(segments.len()); + let mut dst_file = File::create(end_filepath)?; + let mut region_buf = BytesMut::new(); + let mut current = u32::try_from(REGION_START)?; + for region in segments.iter().map(|s| s.region.clone()).unique() { + region_buf.extend_from_slice(region.as_bytes()); + let region_len = region.len() as u32; + region_pool.insert(region, current); + current += region_len; + } + dst_file.seek(SeekFrom::Start(REGION_START))?; + dst_file.write_all(region_buf.as_ref())?; + info!("Load region pool successfully"); + + Ok(Self { + ip_version, + dst_file, + region_pool, + vector_index: [[[0; VECTOR_INDEX_SIZE]; VECTOR_INDEX_ROWS]; VECTOR_INDEX_COLS], + segments, + header, + }) + } + + fn set_vector_index(&mut self, ip: &[u8], ptr: u32) -> Result<()> { + let (l0, l1) = (ip[0] as usize, ip[1] as usize); + let block = &mut self.vector_index[l0][l1]; + + if block[0..4].eq(&[0; 4]) { + block[0..4].copy_from_slice(&ptr.to_le_bytes()); + } + let end_value = ptr + self.ip_version.segment_index_size() as u32; + block[4..].copy_from_slice(&end_value.to_le_bytes()); + Ok(()) + } + + pub fn start(&mut self) -> Result<()> { + let start_index_ptr = u32::try_from(self.dst_file.stream_position()?)?; + let mut count = 0; + let mut buf = + BytesMut::with_capacity(self.ip_version.segment_index_size() * self.segments.len()); + + for segment in std::mem::take(&mut self.segments) { + let region_ptr = *self + .region_pool + .get(&segment.region) + .ok_or(MakerError::RegionNotFound)?; + let region_len = u16::try_from(segment.region.len())?; + + trace!(?segment, "before segment split"); + for seg in segment.split()? { + self.set_vector_index( + &seg.start_ip.ipaddr_bytes(), + start_index_ptr + buf.len() as u32, + )?; + + buf.put_slice(&seg.start_ip.encode_ipaddr_bytes()); + buf.put_slice(&seg.end_ip.encode_ipaddr_bytes()); + buf.put_u16_le(region_len); + buf.put_u32_le(region_ptr); + count += 1; + } + } + + info!("Write segment index buffer"); + self.dst_file + .seek(SeekFrom::Start(start_index_ptr as u64))?; + self.dst_file.write_all(buf.as_ref())?; + + info!("Write header buffer"); + let header_buf = self.header.encode_bytes( + start_index_ptr, + start_index_ptr + (buf.len() as u32) - (self.ip_version.segment_index_size() as u32), + ); + self.dst_file.seek(SeekFrom::Start(0))?; + self.dst_file.write_all(header_buf.as_ref())?; + + self.dst_file + .seek(SeekFrom::Start(HEADER_INFO_LENGTH as u64))?; + self.dst_file + .write_all(self.vector_index.as_flattened().as_flattened())?; + + info!( + start_index_ptr, + current_index_ptr = start_index_ptr + buf.len() as u32, + region_pool_len = self.region_pool.len(), + count, + "Write done" + ); + + Ok(()) + } +} diff --git a/maker/rust/maker/src/segment.rs b/maker/rust/maker/src/segment.rs new file mode 100644 index 0000000..9425eb9 --- /dev/null +++ b/maker/rust/maker/src/segment.rs @@ -0,0 +1,166 @@ +use std::fs::File; +use std::io::{BufRead, BufReader}; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; +use std::str::FromStr; +use std::sync::Arc; + +use itertools::Itertools; +use tracing::{debug, info, trace}; + +use crate::IpVersion; +use crate::error::{MakerError, Result}; +use crate::header::IPAddrExt; + +pub trait IpPlusEq { + fn ip_plus_eq(&self, other: &Self) -> bool; +} + +impl IpPlusEq for IpAddr { + fn ip_plus_eq(&self, other: &Self) -> bool { + match (self, other) { + (IpAddr::V4(start), IpAddr::V4(end)) => Ipv4Addr::from(u32::from(*start) + 1).eq(end), + (IpAddr::V6(start), IpAddr::V6(end)) => Ipv6Addr::from(u128::from(*start) + 1).eq(end), + _ => false, + } + } +} + +#[derive(Debug)] +pub struct Segment { + pub start_ip: IpAddr, + pub end_ip: IpAddr, + pub region: Arc, +} + +fn region_filter(region: &str, filter_fields: &[usize]) -> Result { + if filter_fields.is_empty() { + return Ok(region.to_owned()); + } + let fields = region.split('|').collect::>(); + let filtered = filter_fields + .iter() + .map(|idx| { + fields + .get(*idx) + .ok_or(MakerError::RegionFilterFieldsTooBig { + limit: fields.len(), + actual: *idx, + }) + }) + .collect::>>()?; + Ok(filtered.into_iter().join("|")) +} + +impl Segment { + pub fn from_file( + src_filepath: &str, + ip_version: IpVersion, + filter_fields: &[usize], + ) -> Result> { + info!("Read src file"); + + let mut last = None; + let mut segments = vec![]; + + let reader = BufReader::new(File::open(src_filepath)?); + for original_line in reader.lines() { + let original_line = original_line?; + let line = original_line.trim(); + if line.is_empty() || line.starts_with('#') { + continue; + } + + trace!(?line, "Processing line"); + let v = line.splitn(3, '|').collect::>(); + if v.len() != 3 { + return Err(MakerError::ParseIPRegion(line.to_owned())); + } + let (start_ip, end_ip, region) = (v[0], v[1], v[2]); + + let (start_ip, end_ip) = if ip_version.eq(&IpVersion::V4) { + ( + IpAddr::V4(Ipv4Addr::from_str(start_ip)?), + IpAddr::V4(Ipv4Addr::from_str(end_ip)?), + ) + } else { + ( + IpAddr::V6(Ipv6Addr::from_str(start_ip)?), + IpAddr::V6(Ipv6Addr::from_str(end_ip)?), + ) + }; + if start_ip.gt(&end_ip) { + return Err(MakerError::ParseIPRegion(line.to_owned())); + } + + let segment = Segment { + start_ip, + end_ip, + region: Arc::new(region_filter(region, filter_fields)?), + }; + match last.take() { + None => { + last = Some(segment); + } + Some(mut l) + if segment.region.eq(&l.region) && l.end_ip.ip_plus_eq(&segment.start_ip) => + { + l.end_ip = segment.end_ip; + last = Some(l); + } + Some(seg) => { + segments.push(seg); + last = Some(segment); + } + } + } + + if let Some(last) = last { + segments.push(last); + } + + info!(length = segments.len(), "load segments"); + Ok(segments) + } + + pub fn split(self) -> Result> { + let start_bytes = self.start_ip.ipaddr_bytes(); + let end_bytes = self.end_ip.ipaddr_bytes(); + + let start_byte = u16::from_be_bytes([start_bytes[0], start_bytes[1]]); + let end_byte = u16::from_be_bytes([end_bytes[0], end_bytes[1]]); + + let segments = (start_byte..=end_byte) + .map(|index| { + let sip = if index == start_byte { + self.start_ip + } else if self.start_ip.is_ipv4() { + IpAddr::from(Ipv4Addr::from((index as u32) << 16)) + } else { + IpAddr::from(Ipv6Addr::from((index as u128) << 112)) + }; + + let eip = if index == end_byte { + self.end_ip + } else if self.start_ip.is_ipv4() { + let mask = (1 << 16) - 1; + let v = (index as u32) << 16; + IpAddr::from(Ipv4Addr::from(v | mask)) + } else { + let mask = (1 << 112) - 1; + let v = (index as u128) << 112; + IpAddr::from(Ipv6Addr::from(v | mask)) + }; + + trace!(?index, ?sip, ?eip, ?self.region, "in split segment"); + Segment { + start_ip: sip, + end_ip: eip, + region: self.region.clone(), + } + }) + .collect_vec(); + debug!(?self, length = segments.len(), "Try to index segment"); + + Ok(segments) + } +} diff --git a/maker/rust/vimdiff.png b/maker/rust/vimdiff.png deleted file mode 100644 index 39c220e95a41d089e0b7aaee7e6db581fc271210..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21382 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

I?B@(9))8`r4lv;74 z94WE72SznoYNf&QTsFmqPztbxC=yEBdqezTK`vWovAQlKZL1*`=6u zw^3)jcpD9YQO=gT+g6355nnSC!k;5E%g8EByB`HVk@2BTq4Bvej0bOO2Odk$wr)=w zU(p`AJ`_QSEga%_=XKa1(!+6gy96fobh}4wg?uPLv8E2j&B6uyp9XqwPoYy%GVeYq ze5i1jhnh&8#S{A2$n)^6ubDbD#XQAc0_D}=Yfr*CP{4JOw?Ls*be@+NSq*3G>Bjr$ z&$<()X!Rm{u!W8KjC()|jZ?7USTT9bV3M+}MAa1Ov8*AF3Ewt%)T5WmNoP|w+h_5X z@j89|ldN5aJrHgHXN^NGiXQJ@=SO*%Ex@vS3c+2H2@kl3vhv0OeO|xTZ73fJ33HRv zFQBS&TH5IOnQPf1pf6+MVntTk7D0gjKC@T7)7c?`h$>)Sp`cwm^UI_?L095UyjUi- zg{+oDdC2AfS~p)Ze$v$q7n}2#%f>yUt8A97 z*v#fwx6LGenKuKny@_gjfF$2eUwqt=W? z)Fo1NZf7!u;b=CR;hdR7dtiJ}R1hjkBA&zX^CU=7!E%9xWE?lL^Z-T>*@pQIofV~M z7h(itVvZhj4PY z3}W53i7@nHW?|G8aY(dVtuGu_>#1KUBhqHJ3n#iZnOF6s<1 zKKW9$h?ly0O0P0McygYQ=p#s>$?}#1Y|)lqh|h@&_gv+d;eiXj(>4P{Oz6AzNh;%> zpq%-b<$ixD3IeWRodYc~c^ zmguz1ux0M(XKiF2i*+~dvfOeDOK>Z>eeiS^B8|Bj{s=XCb0iWm281l er<@DUiM(Wa?)IFsDbQVD&b!@qQFk8y