@@ -467,10 +467,41 @@ fn has_scheme(endpoint: &str) -> bool {
467467/// Redacts userinfo from an endpoint string for error messages, so embedded
468468/// basic-auth credentials are not echoed into logs or command output.
469469///
470- /// Only the authority portion (between the scheme and the first `/`, `?`, or
471- /// `#`) is inspected; an `@` later in the path or query is left untouched.
470+ /// Leading scheme prefixes are skipped before inspecting the authority-like
471+ /// segment. Repeating this handles a defaulted `https://` prefix followed by a
472+ /// malformed original prefix such as `http:/`. An `@` later in the path or
473+ /// query is left untouched.
472474fn redact_userinfo ( endpoint : & str ) -> String {
473- let Some ( ( scheme, rest) ) = endpoint. split_once ( "://" ) else {
475+ let mut authority_start = 0 ;
476+ while let Some ( rest) = endpoint. get ( authority_start..) {
477+ let Some ( scheme_end) = rest. find ( ':' ) else {
478+ break ;
479+ } ;
480+ let Some ( scheme) = rest. get ( ..scheme_end) else {
481+ break ;
482+ } ;
483+ let mut chars = scheme. chars ( ) ;
484+ let valid_scheme = matches ! ( chars. next( ) , Some ( c) if c. is_ascii_alphabetic( ) )
485+ && chars. all ( |c| c. is_ascii_alphanumeric ( ) || matches ! ( c, '+' | '-' | '.' ) ) ;
486+ if !valid_scheme {
487+ break ;
488+ }
489+
490+ let after_colon = authority_start + scheme_end + 1 ;
491+ let slash_count = endpoint
492+ . get ( after_colon..)
493+ . unwrap_or_default ( )
494+ . bytes ( )
495+ . take_while ( |byte| * byte == b'/' )
496+ . count ( ) ;
497+ let is_http = scheme. eq_ignore_ascii_case ( "http" ) || scheme. eq_ignore_ascii_case ( "https" ) ;
498+ if slash_count == 0 && !is_http {
499+ break ;
500+ }
501+ authority_start = after_colon + slash_count;
502+ }
503+
504+ let Some ( rest) = endpoint. get ( authority_start..) else {
474505 return endpoint. to_string ( ) ;
475506 } ;
476507 // The authority ends at the first `/`, `?`, or `#`; `find` returns the
@@ -482,7 +513,8 @@ fn redact_userinfo(endpoint: &str) -> String {
482513 let Some ( ( _, host_port) ) = authority. rsplit_once ( '@' ) else {
483514 return endpoint. to_string ( ) ;
484515 } ;
485- format ! ( "{scheme}://<redacted>@{host_port}{tail}" )
516+ let prefix = endpoint. get ( ..authority_start) . unwrap_or_default ( ) ;
517+ format ! ( "{prefix}<redacted>@{host_port}{tail}" )
486518}
487519
488520#[ cfg( test) ]
@@ -671,6 +703,7 @@ mod tests {
671703 for endpoint in [
672704 "http://user:secret@localhost:notaport" ,
673705 "user:secret@exa mple.com" ,
706+ "http:/user:secret@example.com" ,
674707 ] {
675708 let error = HttpEndpoint :: parse ( endpoint) . expect_err ( "endpoint should be rejected" ) ;
676709 let message = error. to_string ( ) ;
0 commit comments