@@ -113,17 +113,39 @@ pub struct Url {
113113 /// The URL scheme.
114114 pub scheme : Scheme ,
115115 /// The user to impersonate on the remote.
116+ ///
117+ /// Stored in decoded form: percent-encoded characters are decoded during parsing.
118+ /// Re-encoded during canonical serialization, but written as-is in alternative form.
116119 pub user : Option < String > ,
117120 /// The password associated with a user.
121+ ///
122+ /// Stored in decoded form: percent-encoded characters are decoded during parsing.
123+ /// Re-encoded during canonical serialization. Cannot be serialized in alternative form (will panic in debug builds).
118124 pub password : Option < String > ,
119125 /// The host to which to connect. Localhost is implied if `None`.
126+ ///
127+ /// IPv6 addresses are stored *without* brackets for SSH schemes, but *with* brackets for other schemes.
128+ /// Brackets are automatically added during serialization when needed (e.g., when a port is specified with an IPv6 host).
120129 pub host : Option < String > ,
121130 /// When serializing, use the alternative forms as it was parsed as such.
131+ ///
132+ /// Alternative forms include SCP-like syntax (`user@host:path`) and bare file paths.
133+ /// When `true`, password and port cannot be serialized (will panic in debug builds).
122134 pub serialize_alternative_form : bool ,
123135 /// The port to use when connecting to a host. If `None`, standard ports depending on `scheme` will be used.
124136 pub port : Option < u16 > ,
125137 /// The path portion of the URL, usually the location of the git repository.
126138 ///
139+ /// Unlike `user` and `password`, paths are stored and serialized in their original form
140+ /// without percent-decoding or re-encoding (e.g., `%20` remains `%20`, not converted to space).
141+ ///
142+ /// Path normalization during parsing:
143+ /// - SSH/Git schemes: Leading `/~` is stripped (e.g., `/~repo` becomes `~repo`)
144+ /// - SSH/Git schemes: Empty paths are rejected as errors
145+ /// - HTTP/HTTPS schemes: Empty paths are normalized to `/`
146+ ///
147+ /// During serialization, SSH/Git URLs prepend `/` to paths not starting with `/`.
148+ ///
127149 /// # Security Warning
128150 ///
129151 /// URLs allow paths to start with `-` which makes it possible to mask command-line arguments as path which then leads to
@@ -381,7 +403,7 @@ impl Url {
381403 out. write_all ( self . scheme . as_str ( ) . as_bytes ( ) ) ?;
382404 out. write_all ( b"://" ) ?;
383405
384- let needs_brackets = self . port . is_some ( ) && self . host . as_ref ( ) . is_some_and ( |h| Self :: is_ipv6 ( h ) ) ;
406+ let needs_brackets = self . port . is_some ( ) && self . host_needs_brackets ( ) ;
385407
386408 match ( & self . user , & self . host ) {
387409 ( Some ( user) , Some ( host) ) => {
@@ -427,20 +449,19 @@ impl Url {
427449 Ok ( ( ) )
428450 }
429451
430- fn is_ipv6 ( host : & str ) -> bool {
431- host. contains ( ':' ) && !host. starts_with ( '[' )
452+ fn host_needs_brackets ( & self ) -> bool {
453+ fn is_ipv6 ( h : & str ) -> bool {
454+ h. contains ( ':' ) && !h. starts_with ( '[' )
455+ }
456+ self . host . as_ref ( ) . is_some_and ( |h| is_ipv6 ( h) )
432457 }
433458
434459 fn write_alternative_form_to ( & self , out : & mut dyn std:: io:: Write ) -> std:: io:: Result < ( ) > {
435- let needs_brackets = self . host . as_ref ( ) . is_some_and ( |h| Self :: is_ipv6 ( h ) ) ;
460+ let needs_brackets = self . host_needs_brackets ( ) ;
436461
437462 match ( & self . user , & self . host ) {
438463 ( Some ( user) , Some ( host) ) => {
439464 out. write_all ( user. as_bytes ( ) ) ?;
440- assert ! (
441- self . password. is_none( ) ,
442- "BUG: cannot serialize password in alternative form"
443- ) ;
444465 out. write_all ( b"@" ) ?;
445466 if needs_brackets {
446467 out. write_all ( b"[" ) ?;
@@ -466,7 +487,6 @@ impl Url {
466487 ) ) ;
467488 }
468489 }
469- assert ! ( self . port. is_none( ) , "BUG: cannot serialize port in alternative form" ) ;
470490 if self . scheme == Scheme :: Ssh {
471491 out. write_all ( b":" ) ?;
472492 }
0 commit comments