@@ -5,13 +5,24 @@ use std::{
55} ;
66
77use landscape_common:: {
8- config:: { dns:: DomainMatchType , geo:: GeoSiteFileConfig } ,
8+ config:: {
9+ dns:: DomainMatchType ,
10+ geo:: { GeoIpError , GeoIpFileFormat , GeoSiteFileConfig } ,
11+ } ,
912 ip_mark:: IpConfig ,
1013} ;
1114use protos:: geo:: { mod_Domain:: Type , Domain , GeoIPListOwned , GeoSiteListOwned } ;
1215
1316mod protos;
1417
18+ pub const DEFAULT_TXT_GEO_KEY : & str = "DEFAULT" ;
19+
20+ pub struct GeoIpParseResult {
21+ pub entries : HashMap < String , Vec < IpConfig > > ,
22+ pub valid_lines : usize ,
23+ pub skipped_lines : usize ,
24+ }
25+
1526pub async fn read_geo_sites_from_bytes (
1627 contents : impl Into < Vec < u8 > > ,
1728) -> HashMap < String , Vec < GeoSiteFileConfig > > {
@@ -69,6 +80,73 @@ pub async fn read_geo_ips_from_bytes(
6980 result
7081}
7182
83+ pub async fn read_geo_ips_from_bytes_dat (
84+ contents : impl Into < Vec < u8 > > ,
85+ ) -> Result < HashMap < String , Vec < IpConfig > > , GeoIpError > {
86+ let mut result = HashMap :: new ( ) ;
87+ let list = GeoIPListOwned :: try_from ( contents. into ( ) ) . map_err ( |_| GeoIpError :: DatDecodeError ) ?;
88+
89+ for entry in list. proto ( ) . entry . iter ( ) {
90+ let domains = entry. cidr . iter ( ) . filter_map ( convert_ipconfig_from_proto) . collect ( ) ;
91+ result. insert ( entry. country_code . to_string ( ) , domains) ;
92+ }
93+
94+ Ok ( result)
95+ }
96+
97+ pub fn read_geo_ips_from_bytes_txt (
98+ contents : impl AsRef < [ u8 ] > ,
99+ txt_key : Option < & str > ,
100+ ) -> Result < GeoIpParseResult , GeoIpError > {
101+ let key = txt_key
102+ . map ( str:: trim)
103+ . filter ( |value| !value. is_empty ( ) )
104+ . unwrap_or ( DEFAULT_TXT_GEO_KEY )
105+ . to_ascii_uppercase ( ) ;
106+
107+ let text = String :: from_utf8_lossy ( contents. as_ref ( ) ) ;
108+ let mut values = Vec :: new ( ) ;
109+ let mut skipped_lines = 0 ;
110+
111+ for line in text. lines ( ) {
112+ let line = line. trim ( ) ;
113+ if line. is_empty ( ) || line. starts_with ( '#' ) {
114+ continue ;
115+ }
116+
117+ if let Some ( cidr) = parse_txt_cidr_line ( line) {
118+ values. push ( cidr) ;
119+ } else {
120+ skipped_lines += 1 ;
121+ }
122+ }
123+
124+ if values. is_empty ( ) {
125+ return Err ( GeoIpError :: NoValidCidrFound ) ;
126+ }
127+
128+ let valid_lines = values. len ( ) ;
129+ let mut entries = HashMap :: new ( ) ;
130+ entries. insert ( key, values) ;
131+
132+ Ok ( GeoIpParseResult { entries, valid_lines, skipped_lines } )
133+ }
134+
135+ pub async fn read_geo_ips_from_bytes_by_format (
136+ contents : impl Into < Vec < u8 > > ,
137+ format : & GeoIpFileFormat ,
138+ txt_key : Option < & str > ,
139+ ) -> Result < GeoIpParseResult , GeoIpError > {
140+ let contents = contents. into ( ) ;
141+ match format {
142+ GeoIpFileFormat :: Dat => {
143+ let entries = read_geo_ips_from_bytes_dat ( contents) . await ?;
144+ Ok ( GeoIpParseResult { entries, valid_lines : 0 , skipped_lines : 0 } )
145+ }
146+ GeoIpFileFormat :: Txt => read_geo_ips_from_bytes_txt ( & contents, txt_key) ,
147+ }
148+ }
149+
72150pub async fn read_geo_ips < T : AsRef < Path > > ( geo_file_path : T ) -> HashMap < String , Vec < IpConfig > > {
73151 let mut result = HashMap :: new ( ) ;
74152 let data = tokio:: fs:: read ( geo_file_path) . await . unwrap ( ) ;
@@ -99,18 +177,36 @@ pub fn convert_ipconfig_from_proto(value: &crate::protos::geo::CIDR) -> Option<I
99177 result. map ( |ip| IpConfig { ip, prefix : value. prefix } )
100178}
101179
180+ fn parse_txt_cidr_line ( line : & str ) -> Option < IpConfig > {
181+ let ( ip, prefix) = line. split_once ( '/' ) ?;
182+ let ip: IpAddr = ip. trim ( ) . parse ( ) . ok ( ) ?;
183+ let prefix: u32 = prefix. trim ( ) . parse ( ) . ok ( ) ?;
184+
185+ let max_prefix = match ip {
186+ IpAddr :: V4 ( _) => 32 ,
187+ IpAddr :: V6 ( _) => 128 ,
188+ } ;
189+
190+ if prefix > max_prefix {
191+ return None ;
192+ }
193+
194+ Some ( IpConfig { ip, prefix } )
195+ }
196+
102197#[ cfg( test) ]
103198#[ global_allocator]
104199static GLOBAL : jemallocator:: Jemalloc = jemallocator:: Jemalloc ;
105200
106201#[ cfg( test) ]
107202mod tests {
203+ use std:: net:: { IpAddr , Ipv4Addr , Ipv6Addr } ;
108204
109205 use jemalloc_ctl:: { epoch, stats} ;
110206
111207 use crate :: {
112208 protos:: geo:: { GeoIPListOwned , GeoSiteListOwned } ,
113- read_geo_sites,
209+ read_geo_ips_from_bytes_txt , read_geo_sites,
114210 } ;
115211
116212 fn test_memory_usage ( ) {
@@ -179,4 +275,41 @@ mod tests {
179275 println ! ( "other count: {sum:?}" ) ;
180276 test_memory_usage ( ) ;
181277 }
278+
279+ #[ test]
280+ fn parse_txt_geo_ips_skips_invalid_lines ( ) {
281+ let result = read_geo_ips_from_bytes_txt (
282+ b"\n # comment\n 1.1.1.0/24\n invalid\n 2001:db8::/32\n 10.0.0.1/33\n " ,
283+ Some ( "custom" ) ,
284+ )
285+ . unwrap ( ) ;
286+
287+ assert_eq ! ( result. valid_lines, 2 ) ;
288+ assert_eq ! ( result. skipped_lines, 2 ) ;
289+ assert_eq ! ( result. entries. len( ) , 1 ) ;
290+
291+ let values = result. entries . get ( "CUSTOM" ) . unwrap ( ) ;
292+ assert_eq ! (
293+ values[ 0 ] ,
294+ landscape_common:: ip_mark:: IpConfig {
295+ ip: IpAddr :: V4 ( Ipv4Addr :: new( 1 , 1 , 1 , 0 ) ) ,
296+ prefix: 24 ,
297+ }
298+ ) ;
299+ assert_eq ! (
300+ values[ 1 ] ,
301+ landscape_common:: ip_mark:: IpConfig {
302+ ip: IpAddr :: V6 ( Ipv6Addr :: from( [
303+ 0x20 , 0x01 , 0x0d , 0xb8 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ,
304+ ] ) ) ,
305+ prefix: 32 ,
306+ }
307+ ) ;
308+ }
309+
310+ #[ test]
311+ fn parse_txt_geo_ips_uses_default_key ( ) {
312+ let result = read_geo_ips_from_bytes_txt ( b"1.1.1.0/24\n " , Some ( " " ) ) . unwrap ( ) ;
313+ assert ! ( result. entries. contains_key( "DEFAULT" ) ) ;
314+ }
182315}
0 commit comments