Skip to content

Commit 68d761c

Browse files
authored
Merge pull request #2060 from yuja/push-urolxnurwtsn
fix: username in scp-like url is no longer percent-encoded (#2056)
2 parents c2eb0c1 + 212b618 commit 68d761c

File tree

4 files changed

+86
-12
lines changed

4 files changed

+86
-12
lines changed

gix-url/src/lib.rs

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -315,11 +315,23 @@ fn percent_encode(s: &str) -> Cow<'_, str> {
315315
/// Serialization
316316
impl Url {
317317
/// Write this URL losslessly to `out`, ready to be parsed again.
318-
pub fn write_to(&self, mut out: &mut dyn std::io::Write) -> std::io::Result<()> {
319-
if !(self.serialize_alternative_form && (self.scheme == Scheme::File || self.scheme == Scheme::Ssh)) {
320-
out.write_all(self.scheme.as_str().as_bytes())?;
321-
out.write_all(b"://")?;
318+
pub fn write_to(&self, out: &mut dyn std::io::Write) -> std::io::Result<()> {
319+
// Since alternative form doesn't employ any escape syntax, password and
320+
// port number cannot be encoded.
321+
if self.serialize_alternative_form
322+
&& (self.scheme == Scheme::File || self.scheme == Scheme::Ssh)
323+
&& self.password.is_none()
324+
&& self.port.is_none()
325+
{
326+
self.write_alternative_form_to(out)
327+
} else {
328+
self.write_canonical_form_to(out)
322329
}
330+
}
331+
332+
fn write_canonical_form_to(&self, out: &mut dyn std::io::Write) -> std::io::Result<()> {
333+
out.write_all(self.scheme.as_str().as_bytes())?;
334+
out.write_all(b"://")?;
323335
match (&self.user, &self.host) {
324336
(Some(user), Some(host)) => {
325337
out.write_all(percent_encode(user).as_bytes())?;
@@ -337,9 +349,31 @@ impl Url {
337349
(Some(_user), None) => unreachable!("BUG: should not be possible to have a user but no host"),
338350
}
339351
if let Some(port) = &self.port {
340-
write!(&mut out, ":{port}")?;
352+
write!(out, ":{port}")?;
353+
}
354+
out.write_all(&self.path)?;
355+
Ok(())
356+
}
357+
358+
fn write_alternative_form_to(&self, out: &mut dyn std::io::Write) -> std::io::Result<()> {
359+
match (&self.user, &self.host) {
360+
(Some(user), Some(host)) => {
361+
out.write_all(user.as_bytes())?;
362+
assert!(
363+
self.password.is_none(),
364+
"BUG: cannot serialize password in alternative form"
365+
);
366+
out.write_all(b"@")?;
367+
out.write_all(host.as_bytes())?;
368+
}
369+
(None, Some(host)) => {
370+
out.write_all(host.as_bytes())?;
371+
}
372+
(None, None) => {}
373+
(Some(_user), None) => unreachable!("BUG: should not be possible to have a user but no host"),
341374
}
342-
if self.serialize_alternative_form && self.scheme == Scheme::Ssh {
375+
assert!(self.port.is_none(), "BUG: cannot serialize port in alternative form");
376+
if self.scheme == Scheme::Ssh {
343377
out.write_all(b":")?;
344378
}
345379
out.write_all(&self.path)?;

gix-url/tests/fixtures/make_baseline.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ tests_windows=()
1515
for path in "repo" "re:po" "re/po"; do
1616
# normal urls
1717
for protocol in "ssh+git" "git+ssh" "git" "ssh"; do
18-
for host in "host" "user@host" "user@[::1]" "user@::1"; do
18+
for host in "host" "user@host" "user_name@host" "user@[::1]" "user@::1"; do
1919
for port_separator in "" ":"; do
2020
tests+=("$protocol://$host$port_separator/$path")
2121

@@ -42,7 +42,7 @@ for path in "repo" "re:po" "re/po"; do
4242
tests+=("./$protocol:$host/~$path")
4343
done
4444
# SCP like urls
45-
for host in "host" "[::1]"; do
45+
for host in "user@name@host" "user_name@host" "host" "[::1]"; do
4646
tests+=("$host:$path")
4747
tests+=("$host:/~$path")
4848
done

gix-url/tests/url/baseline.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ fn run() {
8484
}
8585

8686
assert!(
87-
failure_count_reserialization <= 42,
87+
failure_count_reserialization <= 63,
8888
"the number of reserialization errors should ideally get better, not worse - if this panic is not due to regressions but to new passing test cases, you can set this check to {failure_count_reserialization}"
8989
);
9090
assert_eq!(failure_count_roundtrips, 0, "there should be no roundtrip errors");
@@ -185,8 +185,8 @@ mod baseline {
185185

186186
pub fn max_num_failures(&self) -> usize {
187187
match self {
188-
Kind::Unix => 165,
189-
Kind::Windows => 171,
188+
Kind::Unix => 198,
189+
Kind::Windows => 198 + 6,
190190
}
191191
}
192192

gix-url/tests/url/parse/ssh.rs

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use gix_url::Scheme;
22

3-
use crate::parse::{assert_url, assert_url_roundtrip, url, url_alternate};
3+
use crate::parse::{assert_url, assert_url_roundtrip, url, url_alternate, url_with_pass};
44

55
#[test]
66
fn without_user_and_without_port() -> crate::Result {
@@ -184,6 +184,30 @@ fn scp_like_with_windows_path_and_port_thinks_port_is_part_of_path() -> crate::R
184184
Ok(())
185185
}
186186

187+
#[test]
188+
fn scp_like_with_non_alphanumeric_username() -> crate::Result {
189+
let url = assert_url(
190+
"_user.name@host.xz:C:/path",
191+
url_alternate(Scheme::Ssh, "_user.name", "host.xz", None, b"C:/path"),
192+
)?
193+
.to_bstring();
194+
assert_eq!(url, "_user.name@host.xz:C:/path");
195+
Ok(())
196+
}
197+
198+
// Git passes the non-path part "user@name@host.xz" to OpenSSH, and the ssh
199+
// command interprets it as user = "user@name", host = "host.xz".
200+
#[test]
201+
fn scp_like_with_username_including_at() -> crate::Result {
202+
let url = assert_url(
203+
"user@name@host.xz:path",
204+
url_alternate(Scheme::Ssh, "user@name", "host.xz", None, b"path"),
205+
)?
206+
.to_bstring();
207+
assert_eq!(url, "user@name@host.xz:path");
208+
Ok(())
209+
}
210+
187211
// Git does not care that the host is named `file`, it still treats it as an SCP url.
188212
// I btw tested this, yes you can really clone a repository from there, just `git init`
189213
// in the directory above your home directory on the remote machine.
@@ -193,3 +217,19 @@ fn strange_scp_like_with_host_named_file() -> crate::Result {
193217
assert_eq!(url.to_bstring(), "file:..");
194218
Ok(())
195219
}
220+
221+
#[test]
222+
fn bad_alternative_form_with_password() -> crate::Result {
223+
let url = url_with_pass(Scheme::Ssh, "user", "password", "host.xz", None, b"/")
224+
.serialize_alternate_form(true)
225+
.to_bstring();
226+
assert_eq!(url, "ssh://user:password@host.xz/");
227+
Ok(())
228+
}
229+
230+
#[test]
231+
fn bad_alternative_form_with_port() -> crate::Result {
232+
let url = url_alternate(Scheme::Ssh, None, "host.xz", 21, b"/").to_bstring();
233+
assert_eq!(url, "ssh://host.xz:21/");
234+
Ok(())
235+
}

0 commit comments

Comments
 (0)