@@ -5,7 +5,6 @@ use bitcoin::consensus::encode::Decodable;
55use bitcoin:: consensus:: Encodable ;
66use url:: Url ;
77
8- use super :: error:: { ParseOhttpKeysParamError , ParseReceiverPubkeyParamError } ;
98use crate :: hpke:: HpkePublicKey ;
109use crate :: ohttp:: OhttpKeys ;
1110
@@ -15,7 +14,7 @@ pub(crate) trait UrlExt {
1514 fn set_receiver_pubkey ( & mut self , exp : HpkePublicKey ) ;
1615 fn ohttp ( & self ) -> Result < OhttpKeys , ParseOhttpKeysParamError > ;
1716 fn set_ohttp ( & mut self , ohttp : OhttpKeys ) ;
18- fn exp ( & self ) -> Option < std:: time:: SystemTime > ;
17+ fn exp ( & self ) -> Result < std:: time:: SystemTime , ParseExpParamError > ;
1918 fn set_exp ( & mut self , exp : std:: time:: SystemTime ) ;
2019}
2120
@@ -60,22 +59,23 @@ impl UrlExt for Url {
6059 fn set_ohttp ( & mut self , ohttp : OhttpKeys ) { set_param ( self , "OH1" , & ohttp. to_string ( ) ) }
6160
6261 /// 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 ( ) ?;
62+ fn exp ( & self ) -> Result < std:: time:: SystemTime , ParseExpParamError > {
63+ let value =
64+ get_param ( self , "EX1" , |v| Some ( v . to_owned ( ) ) ) . ok_or ( ParseExpParamError :: MissingExp ) ?;
6665
67- let ex_hrp: Hrp = Hrp :: parse ( "EX" ) . unwrap ( ) ;
68- if hrp != ex_hrp {
69- return None ;
70- }
66+ let ( hrp, bytes) =
67+ crate :: bech32:: nochecksum:: decode ( & value) . map_err ( ParseExpParamError :: DecodeBech32 ) ?;
7168
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- } )
69+ let ex_hrp: Hrp = Hrp :: parse ( "EX" ) . unwrap ( ) ;
70+ if hrp != ex_hrp {
71+ return Err ( ParseExpParamError :: InvalidHrp ( hrp) ) ;
72+ }
73+
74+ u32:: consensus_decode ( & mut & bytes[ ..] )
75+ . map ( |timestamp| {
76+ std:: time:: UNIX_EPOCH + std:: time:: Duration :: from_secs ( timestamp as u64 )
77+ } )
78+ . map_err ( ParseExpParamError :: InvalidExp )
7979 }
8080
8181 /// Set the exp parameter in the URL fragment
@@ -130,6 +130,87 @@ fn set_param(url: &mut Url, prefix: &str, param: &str) {
130130 url. set_fragment ( if fragment. is_empty ( ) { None } else { Some ( & fragment) } ) ;
131131}
132132
133+ #[ cfg( feature = "v2" ) ]
134+ #[ derive( Debug ) ]
135+ pub ( crate ) enum ParseOhttpKeysParamError {
136+ MissingOhttpKeys ,
137+ InvalidOhttpKeys ( crate :: ohttp:: ParseOhttpKeysError ) ,
138+ }
139+
140+ #[ cfg( feature = "v2" ) ]
141+ impl std:: fmt:: Display for ParseOhttpKeysParamError {
142+ fn fmt ( & self , f : & mut std:: fmt:: Formatter < ' _ > ) -> std:: fmt:: Result {
143+ use ParseOhttpKeysParamError :: * ;
144+
145+ match & self {
146+ MissingOhttpKeys => write ! ( f, "ohttp keys are missing" ) ,
147+ InvalidOhttpKeys ( o) => write ! ( f, "invalid ohttp keys: {}" , o) ,
148+ }
149+ }
150+ }
151+
152+ #[ cfg( feature = "v2" ) ]
153+ #[ derive( Debug ) ]
154+ pub ( crate ) enum ParseExpParamError {
155+ MissingExp ,
156+ InvalidHrp ( bitcoin:: bech32:: Hrp ) ,
157+ DecodeBech32 ( bitcoin:: bech32:: primitives:: decode:: CheckedHrpstringError ) ,
158+ InvalidExp ( bitcoin:: consensus:: encode:: Error ) ,
159+ }
160+
161+ #[ cfg( feature = "v2" ) ]
162+ impl std:: fmt:: Display for ParseExpParamError {
163+ fn fmt ( & self , f : & mut std:: fmt:: Formatter < ' _ > ) -> std:: fmt:: Result {
164+ use ParseExpParamError :: * ;
165+
166+ match & self {
167+ MissingExp => write ! ( f, "exp is missing" ) ,
168+ InvalidHrp ( h) => write ! ( f, "incorrect hrp for exp: {}" , h) ,
169+ DecodeBech32 ( d) => write ! ( f, "exp is not valid bech32: {}" , d) ,
170+ InvalidExp ( i) =>
171+ write ! ( f, "exp param does not contain a bitcoin consensus encoded u32: {}" , i) ,
172+ }
173+ }
174+ }
175+
176+ #[ cfg( feature = "v2" ) ]
177+ #[ derive( Debug ) ]
178+ pub ( crate ) enum ParseReceiverPubkeyParamError {
179+ MissingPubkey ,
180+ InvalidHrp ( bitcoin:: bech32:: Hrp ) ,
181+ DecodeBech32 ( bitcoin:: bech32:: primitives:: decode:: CheckedHrpstringError ) ,
182+ InvalidPubkey ( crate :: hpke:: HpkeError ) ,
183+ }
184+
185+ #[ cfg( feature = "v2" ) ]
186+ impl std:: fmt:: Display for ParseReceiverPubkeyParamError {
187+ fn fmt ( & self , f : & mut std:: fmt:: Formatter ) -> std:: fmt:: Result {
188+ use ParseReceiverPubkeyParamError :: * ;
189+
190+ match & self {
191+ MissingPubkey => write ! ( f, "receiver public key is missing" ) ,
192+ InvalidHrp ( h) => write ! ( f, "incorrect hrp for receiver key: {}" , h) ,
193+ DecodeBech32 ( e) => write ! ( f, "receiver public is not valid base64: {}" , e) ,
194+ InvalidPubkey ( e) =>
195+ write ! ( f, "receiver public key does not represent a valid pubkey: {}" , e) ,
196+ }
197+ }
198+ }
199+
200+ #[ cfg( feature = "v2" ) ]
201+ impl std:: error:: Error for ParseReceiverPubkeyParamError {
202+ fn source ( & self ) -> Option < & ( dyn std:: error:: Error + ' static ) > {
203+ use ParseReceiverPubkeyParamError :: * ;
204+
205+ match & self {
206+ MissingPubkey => None ,
207+ InvalidHrp ( _) => None ,
208+ DecodeBech32 ( error) => Some ( error) ,
209+ InvalidPubkey ( error) => Some ( error) ,
210+ }
211+ }
212+ }
213+
133214#[ cfg( test) ]
134215mod tests {
135216 use super :: * ;
@@ -173,7 +254,29 @@ mod tests {
173254 url. set_exp ( exp_time) ;
174255 assert_eq ! ( url. fragment( ) , Some ( "EX1C4UC6ES" ) ) ;
175256
176- assert_eq ! ( url. exp( ) , Some ( exp_time) ) ;
257+ assert_eq ! ( url. exp( ) . unwrap( ) , exp_time) ;
258+ }
259+
260+ #[ test]
261+ fn test_errors_when_parsing_exp ( ) {
262+ let missing_exp_url = Url :: parse ( "http://example.com" ) . unwrap ( ) ;
263+ assert ! ( matches!( missing_exp_url. exp( ) , Err ( ParseExpParamError :: MissingExp ) ) ) ;
264+
265+ let invalid_bech32_exp_url =
266+ Url :: parse ( "http://example.com?pj=https://test-payjoin-url#EX1invalid_bech_32" )
267+ . unwrap ( ) ;
268+ assert ! ( matches!( invalid_bech32_exp_url. exp( ) , Err ( ParseExpParamError :: DecodeBech32 ( _) ) ) ) ;
269+
270+ // Since the HRP is everything to the left of the right-most separator, the invalid url in
271+ // this test would have it's HRP being parsed as EX101 instead of the expected EX1
272+ let invalid_hrp_exp_url =
273+ Url :: parse ( "http://example.com?pj=https://test-payjoin-url#EX1010" ) . unwrap ( ) ;
274+ assert ! ( matches!( invalid_hrp_exp_url. exp( ) , Err ( ParseExpParamError :: InvalidHrp ( _) ) ) ) ;
275+
276+ // Not enough data to decode into a u32
277+ let invalid_timestamp_exp_url =
278+ Url :: parse ( "http://example.com?pj=https://test-payjoin-url#EX10" ) . unwrap ( ) ;
279+ assert ! ( matches!( invalid_timestamp_exp_url. exp( ) , Err ( ParseExpParamError :: InvalidExp ( _) ) ) )
177280 }
178281
179282 #[ test]
0 commit comments