@@ -10,9 +10,40 @@ pub struct GeoContext {
1010 asn : Option < u32 > ,
1111}
1212
13+ #[ derive( Clone , Copy , Debug , Eq , PartialEq ) ]
14+ pub enum GeoContextError {
15+ InvalidCountry ,
16+ InvalidAsn ,
17+ }
18+
19+ impl std:: fmt:: Display for GeoContextError {
20+ fn fmt ( & self , formatter : & mut std:: fmt:: Formatter < ' _ > ) -> std:: fmt:: Result {
21+ match self {
22+ Self :: InvalidCountry => {
23+ formatter. write_str ( "country must contain exactly two ASCII letters" )
24+ }
25+ Self :: InvalidAsn => formatter. write_str ( "ASN must be greater than zero" ) ,
26+ }
27+ }
28+ }
29+
30+ impl std:: error:: Error for GeoContextError { }
31+
1332impl GeoContext {
14- pub fn new ( country_iso : Option < String > , asn : Option < u32 > ) -> Self {
15- Self { country_iso, asn }
33+ pub fn try_new ( country_iso : Option < String > , asn : Option < u32 > ) -> Result < Self , GeoContextError > {
34+ let country_iso = match country_iso {
35+ Some ( value)
36+ if value. len ( ) == 2 && value. bytes ( ) . all ( |byte| byte. is_ascii_alphabetic ( ) ) =>
37+ {
38+ Some ( value. to_ascii_uppercase ( ) )
39+ }
40+ Some ( _) => return Err ( GeoContextError :: InvalidCountry ) ,
41+ None => None ,
42+ } ;
43+ if asn == Some ( 0 ) {
44+ return Err ( GeoContextError :: InvalidAsn ) ;
45+ }
46+ Ok ( Self { country_iso, asn } )
1647 }
1748
1849 pub fn country_iso ( & self ) -> Option < & str > {
@@ -149,7 +180,7 @@ mod geoip_runtime {
149180 }
150181 }
151182
152- let context = GeoContext :: new ( country, asn) ;
183+ let context = GeoContext :: try_new ( country, asn) . ok ( ) ? ;
153184 ( !context. is_empty ( ) ) . then_some ( context)
154185 }
155186 }
@@ -438,38 +469,44 @@ mod geoip_runtime {
438469 }
439470
440471 fn read_verified_mmdb ( verified : VerifiedMmdbFile ) -> io:: Result < Vec < u8 > > {
441- read_verified_mmdb_with_post_read ( verified, || { } )
472+ read_verified_mmdb_with_post_read ( verified, |_ | { } )
442473 }
443474
444475 fn read_verified_mmdb_with_post_read (
445476 mut verified : VerifiedMmdbFile ,
446- post_read : impl FnOnce ( ) ,
477+ post_read : impl FnOnce ( usize ) ,
447478 ) -> io:: Result < Vec < u8 > > {
448- let capacity = usize:: try_from ( verified. byte_len ) . map_err ( |_| {
479+ let expected = usize:: try_from ( verified. byte_len ) . map_err ( |_| {
449480 io:: Error :: new (
450481 io:: ErrorKind :: InvalidInput ,
451482 "GeoIP database size cannot be represented on this platform" ,
452483 )
453484 } ) ?;
454485 let mut contents = Vec :: new ( ) ;
455- contents. try_reserve_exact ( capacity ) . map_err ( |error| {
486+ contents. try_reserve_exact ( expected ) . map_err ( |error| {
456487 io:: Error :: other ( format ! ( "failed to reserve GeoIP database buffer: {error}" ) )
457488 } ) ?;
458- let mut limited = verified
459- . file
460- . by_ref ( )
461- . take ( verified. byte_len . saturating_add ( 1 ) ) ;
462- limited. read_to_end ( & mut contents) ?;
463- if contents. len ( ) as u64 != verified. byte_len {
489+ contents. resize ( expected, 0 ) ;
490+ verified. file . read_exact ( & mut contents) . map_err ( |error| {
491+ io:: Error :: new (
492+ error. kind ( ) ,
493+ format ! (
494+ "GeoIP database became shorter while reading {}: {error}" ,
495+ verified. path. display( )
496+ ) ,
497+ )
498+ } ) ?;
499+ post_read ( contents. capacity ( ) ) ;
500+ let mut extra = [ 0_u8 ; 1 ] ;
501+ if verified. file . read ( & mut extra) ? != 0 {
464502 return Err ( io:: Error :: new (
465503 io:: ErrorKind :: InvalidInput ,
466504 format ! (
467- "GeoIP database size changed while reading: {}" ,
505+ "GeoIP database grew while reading: {}" ,
468506 verified. path. display( )
469507 ) ,
470508 ) ) ;
471509 }
472- post_read ( ) ;
473510 let final_state = MmdbFileState :: from_metadata ( & verified. file . metadata ( ) ?) ;
474511 if final_state != verified. state {
475512 return Err ( io:: Error :: new (
@@ -486,3 +523,32 @@ mod geoip_runtime {
486523
487524#[ cfg( feature = "runtime" ) ]
488525pub use geoip_runtime:: { GeoIpPolicyUsage , GeoIpRuntime } ;
526+
527+ #[ cfg( test) ]
528+ mod geo_context_tests {
529+ use super :: { GeoContext , GeoContextError } ;
530+
531+ #[ test]
532+ fn public_constructor_normalizes_valid_security_context ( ) {
533+ let context = GeoContext :: try_new ( Some ( "se" . to_owned ( ) ) , Some ( 64_512 ) ) . unwrap ( ) ;
534+
535+ assert_eq ! ( context. country_iso( ) , Some ( "SE" ) ) ;
536+ assert_eq ! ( context. asn( ) , Some ( 64_512 ) ) ;
537+ assert ! ( !context. is_empty( ) ) ;
538+ assert ! ( GeoContext :: try_new( None , None ) . unwrap( ) . is_empty( ) ) ;
539+ }
540+
541+ #[ test]
542+ fn public_constructor_rejects_invalid_security_context ( ) {
543+ for country in [ "" , "S" , "USA" , "S1" , "SÉ" , " SE" ] {
544+ assert_eq ! (
545+ GeoContext :: try_new( Some ( country. to_owned( ) ) , None ) ,
546+ Err ( GeoContextError :: InvalidCountry )
547+ ) ;
548+ }
549+ assert_eq ! (
550+ GeoContext :: try_new( Some ( "SE" . to_owned( ) ) , Some ( 0 ) ) ,
551+ Err ( GeoContextError :: InvalidAsn )
552+ ) ;
553+ }
554+ }
0 commit comments