11use std:: borrow:: Cow ;
22use std:: collections:: hash_map:: DefaultHasher ;
3- use std:: fmt;
4- use std:: fmt:: Formatter ;
3+ use std:: fmt:: { self , Display } ;
4+ use std:: fmt:: { Formatter , Write } ;
55use std:: hash:: { Hash , Hasher } ;
66use std:: sync:: OnceLock ;
77
@@ -229,14 +229,38 @@ impl PyUrl {
229229 path : Option < & str > ,
230230 query : Option < & str > ,
231231 fragment : Option < & str > ,
232+ // encode_credentials: bool, // TODO: re-enable this
233+ ) -> PyResult < Bound < ' py , PyAny > > {
234+ Self :: build_inner (
235+ cls, scheme, host, username, password, port, path, query, fragment, false ,
236+ )
237+ }
238+ }
239+
240+ impl PyUrl {
241+ #[ allow( clippy:: too_many_arguments) ]
242+ fn build_inner < ' py > (
243+ cls : & Bound < ' py , PyType > ,
244+ scheme : & str ,
245+ host : & str ,
246+ username : Option < & str > ,
247+ password : Option < & str > ,
248+ port : Option < u16 > ,
249+ path : Option < & str > ,
250+ query : Option < & str > ,
251+ fragment : Option < & str > ,
252+ encode_credentials : bool ,
232253 ) -> PyResult < Bound < ' py , PyAny > > {
233254 let url_host = UrlHostParts {
234255 username : username. map ( Into :: into) ,
235256 password : password. map ( Into :: into) ,
236257 host : Some ( host. into ( ) ) ,
237258 port,
238259 } ;
239- let mut url = format ! ( "{scheme}://{url_host}" ) ;
260+ let mut url = format ! ( "{scheme}://" ) ;
261+ url_host
262+ . to_writer ( & mut url, encode_credentials)
263+ . expect ( "writing to string should not fail" ) ;
240264 if let Some ( path) = path {
241265 url. push ( '/' ) ;
242266 url. push_str ( path) ;
@@ -446,7 +470,30 @@ impl PyMultiHostUrl {
446470 #[ classmethod]
447471 #[ pyo3( signature=( * , scheme, hosts=None , path=None , query=None , fragment=None , host=None , username=None , password=None , port=None ) ) ]
448472 #[ allow( clippy:: too_many_arguments) ]
449- pub fn build < ' py > (
473+ fn build < ' py > (
474+ cls : & Bound < ' py , PyType > ,
475+ scheme : & str ,
476+ hosts : Option < Vec < UrlHostParts > > ,
477+ path : Option < & str > ,
478+ query : Option < & str > ,
479+ fragment : Option < & str > ,
480+ // convenience parameters to build with a single host
481+ host : Option < & str > ,
482+ username : Option < & str > ,
483+ password : Option < & str > ,
484+ port : Option < u16 > ,
485+ // encode_credentials: bool, // TODO: re-enable this
486+ ) -> PyResult < Bound < ' py , PyAny > > {
487+ Self :: build_inner (
488+ cls, scheme, hosts, path, query, fragment, host, username, password, port,
489+ false , // TODO: re-enable this
490+ )
491+ }
492+ }
493+
494+ impl PyMultiHostUrl {
495+ #[ allow( clippy:: too_many_arguments) ]
496+ fn build_inner < ' py > (
450497 cls : & Bound < ' py , PyType > ,
451498 scheme : & str ,
452499 hosts : Option < Vec < UrlHostParts > > ,
@@ -458,39 +505,44 @@ impl PyMultiHostUrl {
458505 username : Option < & str > ,
459506 password : Option < & str > ,
460507 port : Option < u16 > ,
508+ encode_credentials : bool ,
461509 ) -> PyResult < Bound < ' py , PyAny > > {
462- let mut url =
463- if hosts. is_some ( ) && ( host. is_some ( ) || username. is_some ( ) || password. is_some ( ) || port. is_some ( ) ) {
464- return Err ( PyValueError :: new_err (
465- "expected one of `hosts` or singular values to be set." ,
466- ) ) ;
467- } else if let Some ( hosts) = hosts {
468- // check all of host / user / password / port empty
469- // build multi-host url
470- let mut multi_url = format ! ( "{scheme}://" ) ;
471- for ( index, single_host) in hosts. iter ( ) . enumerate ( ) {
472- if single_host. is_empty ( ) {
473- return Err ( PyValueError :: new_err (
474- "expected one of 'host', 'username', 'password' or 'port' to be set" ,
475- ) ) ;
476- }
477- multi_url. push_str ( & single_host. to_string ( ) ) ;
478- if index != hosts. len ( ) - 1 {
479- multi_url. push ( ',' ) ;
480- }
510+ let mut url = format ! ( "{scheme}://" ) ;
511+
512+ if hosts. is_some ( ) && ( host. is_some ( ) || username. is_some ( ) || password. is_some ( ) || port. is_some ( ) ) {
513+ return Err ( PyValueError :: new_err (
514+ "expected one of `hosts` or singular values to be set." ,
515+ ) ) ;
516+ } else if let Some ( hosts) = hosts {
517+ // check all of host / user / password / port empty
518+ // build multi-host url
519+ let len = hosts. len ( ) ;
520+ for ( index, single_host) in hosts. into_iter ( ) . enumerate ( ) {
521+ if single_host. is_empty ( ) {
522+ return Err ( PyValueError :: new_err (
523+ "expected one of 'host', 'username', 'password' or 'port' to be set" ,
524+ ) ) ;
481525 }
482- multi_url
483- } else if host. is_some ( ) {
484- let url_host = UrlHostParts {
485- username : username. map ( Into :: into) ,
486- password : password. map ( Into :: into) ,
487- host : host. map ( Into :: into) ,
488- port,
489- } ;
490- format ! ( "{scheme}://{url_host}" )
491- } else {
492- return Err ( PyValueError :: new_err ( "expected either `host` or `hosts` to be set" ) ) ;
526+ single_host
527+ . to_writer ( & mut url, encode_credentials)
528+ . expect ( "writing to string should not fail" ) ;
529+ if index != len - 1 {
530+ url. push ( ',' ) ;
531+ }
532+ }
533+ } else if host. is_some ( ) {
534+ let url_host = UrlHostParts {
535+ username : username. map ( Into :: into) ,
536+ password : password. map ( Into :: into) ,
537+ host : host. map ( Into :: into) ,
538+ port,
493539 } ;
540+ url_host
541+ . to_writer ( & mut url, encode_credentials)
542+ . expect ( "writing to string should not fail" ) ;
543+ } else {
544+ return Err ( PyValueError :: new_err ( "expected either `host` or `hosts` to be set" ) ) ;
545+ }
494546
495547 if let Some ( path) = path {
496548 url. push ( '/' ) ;
@@ -508,55 +560,65 @@ impl PyMultiHostUrl {
508560 }
509561}
510562
511- pub struct UrlHostParts {
563+ struct UrlHostParts {
512564 username : Option < String > ,
513565 password : Option < String > ,
514566 host : Option < String > ,
515567 port : Option < u16 > ,
516568}
517569
518- impl UrlHostParts {
519- fn is_empty ( & self ) -> bool {
520- self . host . is_none ( ) && self . password . is_none ( ) && self . host . is_none ( ) && self . port . is_none ( )
570+ struct MaybeEncoded < ' a > ( & ' a str , bool ) ;
571+
572+ impl fmt:: Display for MaybeEncoded < ' _ > {
573+ fn fmt ( & self , f : & mut Formatter < ' _ > ) -> fmt:: Result {
574+ if self . 1 {
575+ write ! ( f, "{}" , encode_userinfo_component( self . 0 ) )
576+ } else {
577+ write ! ( f, "{}" , self . 0 )
578+ }
521579 }
522580}
523581
524- impl FromPyObject < ' _ > for UrlHostParts {
525- fn extract_bound ( ob : & Bound < ' _ , PyAny > ) -> PyResult < Self > {
526- let py = ob. py ( ) ;
527- let dict = ob. downcast :: < PyDict > ( ) ?;
528- Ok ( UrlHostParts {
529- username : dict. get_as ( intern ! ( py, "username" ) ) ?,
530- password : dict. get_as ( intern ! ( py, "password" ) ) ?,
531- host : dict. get_as ( intern ! ( py, "host" ) ) ?,
532- port : dict. get_as ( intern ! ( py, "port" ) ) ?,
533- } )
582+ impl UrlHostParts {
583+ fn is_empty ( & self ) -> bool {
584+ self . host . is_none ( ) && self . password . is_none ( ) && self . host . is_none ( ) && self . port . is_none ( )
534585 }
535- }
536586
537- impl fmt:: Display for UrlHostParts {
538- fn fmt ( & self , f : & mut Formatter < ' _ > ) -> fmt:: Result {
587+ fn to_writer ( & self , mut w : impl Write , encode_credentials : bool ) -> fmt:: Result {
539588 match ( & self . username , & self . password ) {
540- ( Some ( username) , None ) => write ! ( f , "{}@" , encode_userinfo_component ( username) ) ?,
541- ( None , Some ( password) ) => write ! ( f , ":{}@" , encode_userinfo_component ( password) ) ?,
589+ ( Some ( username) , None ) => write ! ( w , "{}@" , MaybeEncoded ( username, encode_credentials ) ) ?,
590+ ( None , Some ( password) ) => write ! ( w , ":{}@" , MaybeEncoded ( password, encode_credentials ) ) ?,
542591 ( Some ( username) , Some ( password) ) => write ! (
543- f ,
592+ w ,
544593 "{}:{}@" ,
545- encode_userinfo_component ( username) ,
546- encode_userinfo_component ( password)
594+ MaybeEncoded ( username, encode_credentials ) ,
595+ MaybeEncoded ( password, encode_credentials )
547596 ) ?,
548597 ( None , None ) => { }
549598 }
550599 if let Some ( host) = & self . host {
551- write ! ( f , "{host}" ) ?;
600+ write ! ( w , "{host}" ) ?;
552601 }
553602 if let Some ( port) = self . port {
554- write ! ( f , ":{port}" ) ?;
603+ write ! ( w , ":{port}" ) ?;
555604 }
556605 Ok ( ( ) )
557606 }
558607}
559608
609+ impl FromPyObject < ' _ > for UrlHostParts {
610+ fn extract_bound ( ob : & Bound < ' _ , PyAny > ) -> PyResult < Self > {
611+ let py = ob. py ( ) ;
612+ let dict = ob. downcast :: < PyDict > ( ) ?;
613+ Ok ( UrlHostParts {
614+ username : dict. get_as ( intern ! ( py, "username" ) ) ?,
615+ password : dict. get_as ( intern ! ( py, "password" ) ) ?,
616+ host : dict. get_as ( intern ! ( py, "host" ) ) ?,
617+ port : dict. get_as ( intern ! ( py, "port" ) ) ?,
618+ } )
619+ }
620+ }
621+
560622fn host_to_dict < ' a > ( py : Python < ' a > , lib_url : & Url ) -> PyResult < Bound < ' a , PyDict > > {
561623 let dict = PyDict :: new ( py) ;
562624 dict. set_item ( "username" , Some ( lib_url. username ( ) ) . filter ( |s| !s. is_empty ( ) ) ) ?;
@@ -630,14 +692,10 @@ const USERINFO_ENCODE_SET: &AsciiSet = &CONTROLS
630692 // we must also percent-encode '%'
631693 . add ( b'%' ) ;
632694
633- fn encode_userinfo_component ( value : & str ) -> Cow < ' _ , str > {
634- let encoded = percent_encode ( value. as_bytes ( ) , USERINFO_ENCODE_SET ) . to_string ( ) ;
635- if encoded == value {
636- Cow :: Borrowed ( value)
637- } else {
638- Cow :: Owned ( encoded)
639- }
695+ fn encode_userinfo_component ( value : & str ) -> impl Display + ' _ {
696+ percent_encode ( value. as_bytes ( ) , USERINFO_ENCODE_SET )
640697}
698+
641699// based on https://github.com/servo/rust-url/blob/1c1e406874b3d2aa6f36c5d2f3a5c2ea74af9efb/url/src/parser.rs#L161-L167
642700pub fn scheme_is_special ( scheme : & str ) -> bool {
643701 matches ! ( scheme, "http" | "https" | "ws" | "wss" | "ftp" | "file" )
0 commit comments