use thiserror::Error;
#[derive(Clone, Debug, Error, PartialEq, Eq)]
pub enum TransportPathError {
#[error("fatal: strange pathname '{0}' blocked")]
OptionLikePath(String),
}
#[must_use]
pub fn looks_like_command_line_option(s: &str) -> bool {
!s.is_empty() && s.starts_with('-')
}
pub fn check_local_url_path_not_option_like(url: &str) -> Result<(), TransportPathError> {
let path = url
.strip_prefix("file://")
.unwrap_or(url)
.split('?')
.next()
.unwrap_or("");
if looks_like_command_line_option(path) {
return Err(TransportPathError::OptionLikePath(path.to_owned()));
}
Ok(())
}
#[derive(Clone, Debug, Error, PartialEq, Eq)]
#[error("No directory name could be guessed.\nPlease specify a directory on the command line")]
pub struct NoDirectoryName;
#[inline]
fn is_dir_sep(b: u8) -> bool {
b == b'/'
}
pub fn git_url_basename(
repo: &str,
is_bundle: bool,
is_bare: bool,
) -> Result<String, NoDirectoryName> {
let bytes = repo.as_bytes();
let mut end = bytes.len();
let mut start = match repo.find("://") {
Some(idx) => idx + 3,
None => 0,
};
let mut ptr = start;
while ptr < end && !is_dir_sep(bytes[ptr]) {
if bytes[ptr] == b'@' {
start = ptr + 1;
}
ptr += 1;
}
while start < end && (is_dir_sep(bytes[end - 1]) || bytes[end - 1].is_ascii_whitespace()) {
end -= 1;
}
if end > start + 5 && is_dir_sep(bytes[end - 5]) && &bytes[end - 4..end] == b".git" {
end -= 5;
while start < end && is_dir_sep(bytes[end - 1]) {
end -= 1;
}
}
if end < start {
return Err(NoDirectoryName);
}
let slice = &bytes[start..end];
if !slice.contains(&b'/') && slice.contains(&b':') {
let mut p = end;
while start < p && bytes[p - 1].is_ascii_digit() && bytes[p - 1] != b':' {
p -= 1;
}
if start < p && bytes[p - 1] == b':' {
end = p - 1;
}
}
let mut p = end;
while start < p && !is_dir_sep(bytes[p - 1]) && bytes[p - 1] != b':' {
p -= 1;
}
start = p;
let suffix: &[u8] = if is_bundle { b".bundle" } else { b".git" };
let mut len = end - start;
if len >= suffix.len() && &bytes[start + len - suffix.len()..start + len] == suffix {
len -= suffix.len();
}
if len == 0 || (len == 1 && bytes[start] == b'/') {
return Err(NoDirectoryName);
}
let core = &repo[start..start + len];
let mut dir = if is_bare {
format!("{core}.git")
} else {
core.to_string()
};
dir = collapse_control_and_whitespace(&dir);
Ok(dir)
}
fn collapse_control_and_whitespace(dir: &str) -> String {
let mut out = String::with_capacity(dir.len());
let mut prev_space = true; for &b in dir.as_bytes() {
let ch = if b < 0x20 { b' ' } else { b };
if ch.is_ascii_whitespace() {
if prev_space {
continue;
}
prev_space = true;
} else {
prev_space = false;
}
out.push(ch as char);
}
if prev_space {
while out.ends_with(' ') {
out.pop();
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn basename(url: &str) -> String {
git_url_basename(url, false, false).expect("dir name")
}
#[test]
fn scp_style_basic() {
assert_eq!(basename("host:foo"), "foo");
assert_eq!(basename("host:foo.git"), "foo");
assert_eq!(basename("host:foo/.git"), "foo");
}
#[test]
fn ssh_url_basic() {
assert_eq!(basename("ssh://host/foo"), "foo");
assert_eq!(basename("ssh://host/foo.git"), "foo");
assert_eq!(basename("ssh://host/foo/.git"), "foo");
}
#[test]
fn trailing_slashes_and_git() {
assert_eq!(basename("ssh://host/foo/"), "foo");
assert_eq!(basename("ssh://host/foo///"), "foo");
assert_eq!(basename("ssh://host/foo/.git/"), "foo");
assert_eq!(basename("ssh://host/foo.git/"), "foo");
assert_eq!(basename("ssh://host/foo.git///"), "foo");
assert_eq!(basename("ssh://host/foo///.git/"), "foo");
assert_eq!(basename("ssh://host/foo/.git///"), "foo");
assert_eq!(basename("host:foo/"), "foo");
assert_eq!(basename("host:foo///"), "foo");
assert_eq!(basename("host:foo.git/"), "foo");
assert_eq!(basename("host:foo/.git/"), "foo");
assert_eq!(basename("host:foo.git///"), "foo");
assert_eq!(basename("host:foo///.git/"), "foo");
assert_eq!(basename("host:foo/.git///"), "foo");
}
#[test]
fn empty_path_defaults_to_hostname() {
assert_eq!(basename("ssh://host/"), "host");
assert_eq!(basename("ssh://host:1234/"), "host");
assert_eq!(basename("ssh://user@host/"), "host");
assert_eq!(basename("host:/"), "host");
}
#[test]
fn auth_material_is_redacted() {
assert_eq!(basename("ssh://user:password@host/"), "host");
assert_eq!(basename("ssh://user:password@host:1234/"), "host");
assert_eq!(basename("ssh://user:passw@rd@host:1234/"), "host");
assert_eq!(basename("user@host:/"), "host");
assert_eq!(basename("user:password@host:/"), "host");
assert_eq!(basename("user:passw@rd@host:/"), "host");
}
#[test]
fn auth_like_material_kept_in_path() {
assert_eq!(basename("ssh://host/foo@bar"), "foo@bar");
assert_eq!(basename("ssh://host/foo@bar.git"), "foo@bar");
assert_eq!(basename("ssh://user:password@host/foo@bar"), "foo@bar");
assert_eq!(basename("ssh://user:passw@rd@host/foo@bar.git"), "foo@bar");
assert_eq!(basename("host:/foo@bar"), "foo@bar");
assert_eq!(basename("host:/foo@bar.git"), "foo@bar");
assert_eq!(basename("user:password@host:/foo@bar"), "foo@bar");
assert_eq!(basename("user:passw@rd@host:/foo@bar.git"), "foo@bar");
}
#[test]
fn trailing_port_like_numbers_in_path_kept() {
assert_eq!(basename("ssh://user:password@host/test:1234"), "1234");
assert_eq!(basename("ssh://user:password@host/test:1234.git"), "1234");
}
#[test]
fn bare_appends_git() {
assert_eq!(
git_url_basename("host:foo", false, true).unwrap(),
"foo.git"
);
assert_eq!(
git_url_basename("host:foo.git", false, true).unwrap(),
"foo.git"
);
}
#[test]
fn empty_name_is_error() {
assert_eq!(git_url_basename("/", false, false), Err(NoDirectoryName));
assert_eq!(git_url_basename("", false, false), Err(NoDirectoryName));
}
}