@@ -5,7 +5,7 @@ use bitcoin::consensus::encode::Decodable;
55use bitcoin:: consensus:: Encodable ;
66use url:: Url ;
77
8- use super :: error:: { ParseOhttpKeysParamError , ParseReceiverPubkeyParamError } ;
8+ use super :: error:: { ParseExpParamError , ParseOhttpKeysParamError , ParseReceiverPubkeyParamError } ;
99use crate :: hpke:: HpkePublicKey ;
1010use crate :: ohttp:: OhttpKeys ;
1111
@@ -15,7 +15,7 @@ pub(crate) trait UrlExt {
1515 fn set_receiver_pubkey ( & mut self , exp : HpkePublicKey ) ;
1616 fn ohttp ( & self ) -> Result < OhttpKeys , ParseOhttpKeysParamError > ;
1717 fn set_ohttp ( & mut self , ohttp : OhttpKeys ) ;
18- fn exp ( & self ) -> Option < std:: time:: SystemTime > ;
18+ fn exp ( & self ) -> Result < std:: time:: SystemTime , ParseExpParamError > ;
1919 fn set_exp ( & mut self , exp : std:: time:: SystemTime ) ;
2020}
2121
@@ -60,22 +60,23 @@ impl UrlExt for Url {
6060 fn set_ohttp ( & mut self , ohttp : OhttpKeys ) { set_param ( self , "OH1" , & ohttp. to_string ( ) ) }
6161
6262 /// Retrieve the exp parameter from the URL fragment
63- fn exp ( & self ) -> Option < std:: time:: SystemTime > {
64- get_param ( self , "EX1" , | value| {
65- let ( hrp , bytes ) = crate :: bech32 :: nochecksum :: decode ( value ) . ok ( ) ?;
63+ fn exp ( & self ) -> Result < std:: time:: SystemTime , ParseExpParamError > {
64+ let value =
65+ get_param ( self , "EX1" , |v| Some ( v . to_owned ( ) ) ) . ok_or ( ParseExpParamError :: MissingExp ) ?;
6666
67- let ex_hrp: Hrp = Hrp :: parse ( "EX" ) . unwrap ( ) ;
68- if hrp != ex_hrp {
69- return None ;
70- }
67+ let ( hrp, bytes) =
68+ crate :: bech32:: nochecksum:: decode ( & value) . map_err ( ParseExpParamError :: DecodeBech32 ) ?;
7169
72- let mut cursor = & bytes[ ..] ;
73- u32:: consensus_decode ( & mut cursor)
74- . map ( |timestamp| {
75- std:: time:: UNIX_EPOCH + std:: time:: Duration :: from_secs ( timestamp as u64 )
76- } )
77- . ok ( )
78- } )
70+ let ex_hrp: Hrp = Hrp :: parse ( "EX" ) . unwrap ( ) ;
71+ if hrp != ex_hrp {
72+ return Err ( ParseExpParamError :: InvalidHrp ( hrp) ) ;
73+ }
74+
75+ u32:: consensus_decode ( & mut & bytes[ ..] )
76+ . map ( |timestamp| {
77+ std:: time:: UNIX_EPOCH + std:: time:: Duration :: from_secs ( timestamp as u64 )
78+ } )
79+ . map_err ( ParseExpParamError :: InvalidExp )
7980 }
8081
8182 /// Set the exp parameter in the URL fragment
@@ -173,7 +174,29 @@ mod tests {
173174 url. set_exp ( exp_time) ;
174175 assert_eq ! ( url. fragment( ) , Some ( "EX1C4UC6ES" ) ) ;
175176
176- assert_eq ! ( url. exp( ) , Some ( exp_time) ) ;
177+ assert_eq ! ( url. exp( ) . unwrap( ) , exp_time) ;
178+ }
179+
180+ #[ test]
181+ fn test_errors_when_parsing_exp ( ) {
182+ let missing_exp_url = Url :: parse ( "http://example.com" ) . unwrap ( ) ;
183+ assert ! ( matches!( missing_exp_url. exp( ) , Err ( ParseExpParamError :: MissingExp ) ) ) ;
184+
185+ let invalid_bech32_exp_url =
186+ Url :: parse ( "http://example.com?pj=https://test-payjoin-url#EX1invalid_bech_32" )
187+ . unwrap ( ) ;
188+ assert ! ( matches!( invalid_bech32_exp_url. exp( ) , Err ( ParseExpParamError :: DecodeBech32 ( _) ) ) ) ;
189+
190+ // Since the HRP is everything to the left of the right-most separator, the invalid url in
191+ // this test would have it's HRP being parsed as EX101 instead of the expected EX1
192+ let invalid_hrp_exp_url =
193+ Url :: parse ( "http://example.com?pj=https://test-payjoin-url#EX1010" ) . unwrap ( ) ;
194+ assert ! ( matches!( invalid_hrp_exp_url. exp( ) , Err ( ParseExpParamError :: InvalidHrp ( _) ) ) ) ;
195+
196+ // Not enough data to decode into a u32
197+ let invalid_timestamp_exp_url =
198+ Url :: parse ( "http://example.com?pj=https://test-payjoin-url#EX10" ) . unwrap ( ) ;
199+ assert ! ( matches!( invalid_timestamp_exp_url. exp( ) , Err ( ParseExpParamError :: InvalidExp ( _) ) ) )
177200 }
178201
179202 #[ test]
0 commit comments