@@ -70,7 +70,21 @@ pub(crate) fn find_scheme(input: &BStr) -> InputScheme {
7070 return InputScheme :: Url { protocol_end } ;
7171 }
7272
73- if let Some ( colon) = input. find_byte ( b':' ) {
73+ // Find colon, but skip over IPv6 brackets if present
74+ let colon = if input. starts_with ( b"[" ) {
75+ // IPv6 address, find the closing bracket first
76+ if let Some ( bracket_end) = input. find_byte ( b']' ) {
77+ // Look for colon after the bracket
78+ input[ bracket_end + 1 ..] . find_byte ( b':' ) . map ( |pos| bracket_end + 1 + pos)
79+ } else {
80+ // No closing bracket, treat as regular search
81+ input. find_byte ( b':' )
82+ }
83+ } else {
84+ input. find_byte ( b':' )
85+ } ;
86+
87+ if let Some ( colon) = colon {
7488 // allow user to select files containing a `:` by passing them as absolute or relative path
7589 // this is behavior explicitly mentioned by the scp and git manuals
7690 let explicitly_local = & input[ ..colon] . contains ( & b'/' ) ;
@@ -111,20 +125,57 @@ pub(crate) fn url(input: &BStr, protocol_end: usize) -> Result<crate::Url, Error
111125 // Normalize empty path to "/" for http/https URLs only
112126 let path = if url. path . is_empty ( ) && matches ! ( scheme, Scheme :: Http | Scheme :: Https ) {
113127 "/" . into ( )
128+ } else if matches ! ( scheme, Scheme :: Ssh | Scheme :: Git ) && url. path . starts_with ( "/~" ) {
129+ // For SSH and Git protocols, strip leading '/' from paths starting with '~'
130+ // e.g., "ssh://host/~repo" -> path is "~repo", not "/~repo"
131+ url. path [ 1 ..] . into ( )
114132 } else {
115133 url. path . into ( )
116134 } ;
117135
136+ let user = url_user ( & url, UrlKind :: Url ) ?;
137+ let password = url
138+ . password
139+ . map ( |s| percent_decoded_utf8 ( s, UrlKind :: Url ) )
140+ . transpose ( ) ?;
141+ let port = url. port ;
142+
143+ // For SSH URLs, strip brackets from IPv6 addresses
144+ let host = if scheme == Scheme :: Ssh {
145+ url. host . map ( |mut h| {
146+ // Check if we have bracketed IPv6 with trailing colon: "[::1]:"
147+ if h. starts_with ( '[' ) {
148+ if h. ends_with ( "]:" ) {
149+ // "[::1]:" -> "::1" (strip brackets and colon)
150+ h = h[ 1 ..h. len ( ) - 2 ] . to_string ( ) ;
151+ } else if h. ends_with ( ']' ) {
152+ // "[::1]" -> "::1" (just strip brackets)
153+ h = h[ 1 ..h. len ( ) - 1 ] . to_string ( ) ;
154+ }
155+ } else {
156+ // For non-bracketed hosts, only strip trailing colon if it's not part of IPv6
157+ // Count colons: if there's only one colon and it's at the end, strip it
158+ // Otherwise (multiple colons or colon not at end), keep it
159+ let colon_count = h. chars ( ) . filter ( |& c| c == ':' ) . count ( ) ;
160+ if colon_count == 1 && h. ends_with ( ':' ) {
161+ // Regular host with empty port "host:" -> "host"
162+ h = h[ ..h. len ( ) - 1 ] . to_string ( ) ;
163+ }
164+ // For bare IPv6 with trailing colon "::1:", keep it as is (colon_count > 1)
165+ }
166+ h
167+ } )
168+ } else {
169+ url. host
170+ } ;
171+
118172 Ok ( crate :: Url {
119173 serialize_alternative_form : false ,
120174 scheme,
121- user : url_user ( & url, UrlKind :: Url ) ?,
122- password : url
123- . password
124- . map ( |s| percent_decoded_utf8 ( s, UrlKind :: Url ) )
125- . transpose ( ) ?,
126- host : url. host ,
127- port : url. port ,
175+ user,
176+ password,
177+ host,
178+ port,
128179 path,
129180 } )
130181}
@@ -166,16 +217,37 @@ pub(crate) fn scp(input: &BStr, colon: usize) -> Result<crate::Url, Error> {
166217 source,
167218 } ) ?;
168219
220+ // For SCP-like SSH URLs, strip leading '/' from paths starting with '/~'
221+ // e.g., "user@host:/~repo" -> path is "~repo", not "/~repo"
222+ let path = if path. starts_with ( "/~" ) {
223+ & path[ 1 ..]
224+ } else {
225+ path
226+ } ;
227+
228+ let user = url_user ( & url, UrlKind :: Scp ) ?;
229+ let password = url
230+ . password
231+ . map ( |s| percent_decoded_utf8 ( s, UrlKind :: Scp ) )
232+ . transpose ( ) ?;
233+ let port = url. port ;
234+
235+ // For SCP-like SSH URLs, strip brackets from IPv6 addresses
236+ let host = url. host . map ( |h| {
237+ if h. starts_with ( '[' ) && h. ends_with ( ']' ) {
238+ h[ 1 ..h. len ( ) - 1 ] . to_string ( )
239+ } else {
240+ h
241+ }
242+ } ) ;
243+
169244 Ok ( crate :: Url {
170245 serialize_alternative_form : true ,
171246 scheme : Scheme :: from ( url. scheme . as_str ( ) ) ,
172- user : url_user ( & url, UrlKind :: Scp ) ?,
173- password : url
174- . password
175- . map ( |s| percent_decoded_utf8 ( s, UrlKind :: Scp ) )
176- . transpose ( ) ?,
177- host : url. host ,
178- port : url. port ,
247+ user,
248+ password,
249+ host,
250+ port,
179251 path : path. into ( ) ,
180252 } )
181253}
0 commit comments