pub fn split_scheme(spec: &str) -> (Option<&str>, &str) {
match spec.split_once("://") {
Some((scheme, rest)) => (Some(scheme), rest),
None => (None, spec),
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SshTarget {
pub remote: String,
pub port: Option<u16>,
}
pub fn parse_ssh_target(rest: &str) -> Result<SshTarget, &'static str> {
let (user, hostpart) = match rest.split_once('@') {
Some(("", _)) => return Err("empty user before `@`"),
Some((u, h)) => (Some(u), h),
None => (None, rest),
};
let colon = hostpart.find(':');
let slash = hostpart.find('/');
let (host, port, path) = match (colon, slash) {
(Some(c), s) if s.is_none_or(|s| c < s) => {
let after = &hostpart[c + 1..];
if after.starts_with('/') {
(&hostpart[..c], None, after)
} else {
let end = after.find('/').ok_or("no remote path")?;
let port_str = after[..end].strip_suffix(':').unwrap_or(&after[..end]);
let port = port_str
.parse::<u16>()
.map_err(|_| "port is not a number")?;
(&hostpart[..c], Some(port), &after[end..])
}
}
(_, Some(s)) => (&hostpart[..s], None, &hostpart[s..]),
(_, None) => return Err("no remote path"),
};
if host.is_empty() {
return Err("empty host");
}
if path.len() < 2 {
return Err("the remote path must be absolute");
}
Ok(SshTarget {
remote: match user {
Some(u) => format!("{u}@{host}:{path}"),
None => format!("{host}:{path}"),
},
port,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_scheme_is_only_a_scheme_with_its_separator() {
assert_eq!(
split_scheme("sshfs://exa/data"),
(Some("sshfs"), "exa/data")
);
assert_eq!(split_scheme("/flodl/data"), (None, "/flodl/data"));
assert_eq!(split_scheme("exa:/flodl/data"), (None, "exa:/flodl/data"));
}
#[test]
fn an_ssh_target_parses_all_four_spellings() {
assert_eq!(
parse_ssh_target("flodl@exa:/flodl/data").unwrap(),
SshTarget {
remote: "flodl@exa:/flodl/data".into(),
port: None
},
);
assert_eq!(
parse_ssh_target("exa/flodl/data").unwrap(),
SshTarget {
remote: "exa:/flodl/data".into(),
port: None
},
);
assert_eq!(
parse_ssh_target("flodl@exa:2222/flodl/data").unwrap(),
SshTarget {
remote: "flodl@exa:/flodl/data".into(),
port: Some(2222)
},
);
assert_eq!(
parse_ssh_target("flodl@exa:2222:/flodl/data").unwrap(),
SshTarget {
remote: "flodl@exa:/flodl/data".into(),
port: Some(2222)
},
);
}
#[test]
fn an_ssh_target_says_why_it_refused() {
for (spec, why) in [
("exa", "no remote path"),
("exa:2222", "no remote path"),
("exa:banana/data", "port is not a number"),
("@exa:/flodl/data", "empty user before `@`"),
(":/flodl/data", "empty host"),
("/flodl/data", "empty host"),
("exa:/", "the remote path must be absolute"),
] {
assert_eq!(parse_ssh_target(spec), Err(why), "for {spec}");
}
}
}